Search code examples
javaruby-on-railsrubyencryptionattr-encrypted

trying to decrypt attr_encrypted stored value from Java


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?


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