Question

I have a rails application that encrypts (with attr_encrypted) 2 fields in one of the models.

Another part of my process, which is not the web-application needs to perform some tasks using this data (plaintext).

I'm trying to read the stored values from the DB and decrypt them but just can't..

my model looks like this:

class SecretData < ActiveRecord::Base
  mysecret = "mylittlesecret"

  attr_encrypted :data1, :key=>mysecret, :algorithm => "aes-256-cbc"
  attr_encrypted :data2, :key=>mysecret, :algorithm => "aes-256-cbc"

  ...
end

The DB fields (encrypted_data1 and encrypted_data2) are filled with data but when I try to decode the base64 (attr_encrypted does that by default) and decrypt (I tried with openssl from commandline and using Java) I get "bad magic number" (openssl) or various errors about key length (in Java). I spent a lot of time trying to decrypt those strings but just couldn't find the way.

Here is all the data I have:
encrypted + base64 strings (for data1 and data2) are:

cyE3jDkKc99GVB8TiUlBxQ==
sqcbOnBTl6yy3wwjkl0qhA==

I can decode base64 from both of them and get some byte array. When I try:

echo cyE3jDkKc99GVB8TiUlBxQ== | openssl aes-256-cbc -a -d   (and type "mylittlesecret" as the password)

I get: "bad magic number"

When I try the following Java code:

Key key = generateKey();
Cipher c = Cipher.getInstance(ALGO);
c.init(Cipher.DECRYPT_MODE, key);
byte[] decordedValue = new BASE64Decoder().decodeBuffer(encryptedData);
byte[] decValue = c.doFinal(decordedValue);
String decryptedValue = new String(decValue);

I get "java.security.InvalidKeyException: Invalid AES key length: 14 bytes"
I've tried many variations for the Java code, so it might be that this particular one is a complete mistake..

When I try in ruby:

irb(main):069:0> Encryptor.decrypt(Base64.decode64("cyE3jDkKc99GVB8TiUlBxQ=="), ,key=>'mylittlesecret')
=> "data1-value"

I get the correct value decrypted (as you can see).

I've also noticed that when I try to encrypt the same string in Java and encode in Base64 I get a longer string (after base64). Don't know why but it's probably related..

I thought I should also have a salt/iv with the encrypted value, but I don't see it stored anywhere.. I tried to encrypt the same value twice and got the same output string so it's not a random one.

Does anyone know how does attr_encrypted (it's using ruby's Encryptor) encrypts data and how I should decrypt it externally?

Was it helpful?

Solution

Well, thanks to owlstead I was able to solve this. I'm posting the code in ruby and Java in case someone needs it in the future:

The problem, as owlstead mentioned, is indeed in the EVP_BytesToKey (key generation from a password and salt). Ruby from some reason doesn't use the standard one and therefore Java (or openssl) can't decode.

Here is a ruby implementation that uses a standard method:

def self.encrypt(options)

   plaintext = options[:value]
   return true if plaintext.blank?

   cipher = OpenSSL::Cipher::Cipher.new(@@cipher_type)
   cipher.encrypt

   iv = cipher.random_iv
   salt = (0 ... @@salt_length).map{65.+(rand(25)).chr}.join   # random salt
   key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(@@password, salt, @@pkbdf_num_iters, cipher.key_len)

   cipher.key = key
   cipher.iv = iv

   enc_data = cipher.update(plaintext)
   enc_data << cipher.final

   final_data = salt << iv << enc_data
   Base64.strict_encode64(final_data)
end

def self.decrypt(options)

   ciphertext = options[:value]
   return true if ciphertext.blank?


   cipher = OpenSSL::Cipher::Cipher.new(@@cipher_type)
   cipher.decrypt

   cipher_data = Base64.decode64(ciphertext)

   salt = cipher_data[0 .. @@salt_length-1]
   iv = cipher_data[@@salt_length .. @@salt_length+cipher.iv_len]
   enc_data = cipher_data[@@salt_length+cipher.iv_len .. -1]  # the rest

   key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(@@password, salt, @@pkbdf_num_iters, cipher.key_len)

   cipher.key = key
   cipher.iv = iv

   plaintext = cipher.update(enc_data)
   plaintext << cipher.final

   plaintext
  end

I've set the following parameters: - cipher_type = aes-128-cbc (Java supports only 128 but out of the box. For more than that you need to install some additional packages) - salt_length = 8 - pkbdf_num_iters = 1024

This is the Java method for decoding:

public String decrypt(String ciphertext) throws Exception {
    byte[] crypt = Base64.decodeBase64(ciphertext);

    // parse the encrypted data and get salt and IV
    byte[] salt = Arrays.copyOfRange(crypt, 0, saltLength);
    byte[] iv = Arrays.copyOfRange(crypt, saltLength, saltLength + ivLength);
    byte[] encryptedData = Arrays.copyOfRange(crypt, saltLength + ivLength, crypt.length);

    // generate key from salt and password  
    SecretKeyFactory f = SecretKeyFactory.getInstance(secretKeyName);
    KeySpec ks = new PBEKeySpec(password.toCharArray(), salt, pbkdfNumIters, keyLength);
    SecretKey s = f.generateSecret(ks);
    Key keySpec = new SecretKeySpec(s.getEncoded(),"AES");

    // initialize the cipher object with the key and IV
    Cipher cipher = Cipher.getInstance(cipherAlgo);
    IvParameterSpec ivSpec = new IvParameterSpec(iv);
    cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);

    // decrypt
    byte[] decBytes = cipher.doFinal(encryptedData);

    return new String(decBytes);
}

Worked for me.

Hope it helps (or will, to someone..)

Zach

OTHER TIPS

You will need -nosalt to decrypt the data with OpenSSL. For Java you will need an implementation of the OpenSSL EVP_BytesToKey method. One implementation can be found on the blog of Ola Bini. Thanks for putting this in the public domain, Ola.

    public static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md,
            byte[] salt, byte[] data, int count) {
        byte[][] both = new byte[2][];
        byte[] key = new byte[key_len];
        int key_ix = 0;
        byte[] iv = new byte[iv_len];
        int iv_ix = 0;
        both[0] = key;
        both[1] = iv;
        byte[] md_buf = null;
        int nkey = key_len;
        int niv = iv_len;
        int i = 0;
        if (data == null) {
            return both;
        }
        int addmd = 0;
        for (;;) {
            md.reset();
            if (addmd++ > 0) {
                md.update(md_buf);
            }
            md.update(data);
            if (null != salt) {
                md.update(salt, 0, 8);
            }
            md_buf = md.digest();
            for (i = 1; i < count; i++) {
                md.reset();
                md.update(md_buf);
                md_buf = md.digest();
            }
            i = 0;
            if (nkey > 0) {
                for (;;) {
                    if (nkey == 0)
                        break;
                    if (i == md_buf.length)
                        break;
                    key[key_ix++] = md_buf[i];
                    nkey--;
                    i++;
                }
            }
            if (niv > 0 && i != md_buf.length) {
                for (;;) {
                    if (niv == 0)
                        break;
                    if (i == md_buf.length)
                        break;
                    iv[iv_ix++] = md_buf[i];
                    niv--;
                    i++;
                }
            }
            if (nkey == 0 && niv == 0) {
                break;
            }
        }
        for (i = 0; i < md_buf.length; i++) {
            md_buf[i] = 0;
        }
        return both;
    }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top