Decrypting salted AES file generated on command line with Ruby
OpenSSL uses a custom header and key derivation routine. Security.SE has a good description of the header and the docs for EVP_BytesToKey
describe the key derivation.
We can modify your code to use this weird and somewhat broken key derivation as follows:
encoded_and_encrypted_text = File.read my_file_path
encrypted_text = Base64.decode64 encoded_and_encrypted_text.to_s.strip
header = encrypted_text[0,8]
salt = encrypted_text[8,8]
payload = encrypted_text[16..-1]
decipher = OpenSSL::Cipher::AES.new 256, :CBC
decipher.decrypt
D_1 = OpenSSL::Digest::MD5.new(my_password + salt).digest
D_2 = OpenSSL::Digest::MD5.new(D_1 + my_password + salt).digest
D_3 = OpenSSL::Digest::MD5.new(D_2 + my_password + salt).digest
decipher.key = (D_1 + D_2)
decipher.iv = D_3
plain_text = decipher.update(payload) + decipher.final
Why can't I make OpenSSL with Ruby and Command line OpenSSL interoperable?
You need to use the -K
(upper case) and -iv
options on the command line to specify key and IV explicitly as a string of hex digits. If you use -k
(lower case), OpenSSL will derive key and IV from the password using a key derivation function. When OpenSSL derives a key, it will also use a "salted" ciphertext format which is incompatible with the plain blockwise CBC you are expecting.
Note that in your Ruby code, you are using the first 256 bits (32 bytes) of an ASCII string directly as a key, which is almost certainly not what you want for a real world application where security is an issue. You should use a (randomly generated) binary key, or derive a key from a password using a key derivation function such as PBKDF2, bcrypt or scrypt.
Trying to decrypt a string using openssl/golang which has been encrypted in rails
Trying to decrypt data encrypted in a different system will not work unless you are aware and deal with the many intricate details of how both systems do the cryptography. Although both Rails and the openssl
command line tool use the OpenSSL libraries under the hood for their crypto operations, they both use it in their own distinct ways that are not directly interoperable.
If you look close to the two systems, you'll see that for example:
- Rails message encryptor not only encrypts the message but also signs it
- Rails encryptor uses
Marshal
to serialize the input data - the
openssl enc
tool expects the encrypted data in a distinct file format with aSalted__<salt>
header (this is why you get the bad magic number message fromopenssl
) - the
openssl
tool must be properly configured to use the same ciphers as Rails encryptor and key generator, asopenssl
defaults are different from Rails defaults - the default ciphers configuration changed significantly since Rails 5.2.
With this general info, we can have a look at a a practical example. It is tested in Rails 4.2 but should work equally up to Rails 5.1.
Anatomy of a Rails-encrypted message
Let me start with a slightly amended code that you presented. The only changes there are to preset the password
and salt
to static values and print a lot of debug info:
def encrypt_text(text_to_encrypt)
password = "password" # the password to derive the key
salt = "saltsalt" # salt must be 8 bytes
key = ActiveSupport::KeyGenerator.new(password).generate_key(salt, 32)
puts "salt (hexa) = #{salt.unpack('H*').first}" # print the saltin HEX
puts "key (hexa) = #{key.unpack('H*').first}" # print the generated key in HEX
crypt = ActiveSupport::MessageEncryptor.new(key)
output = crypt.encrypt_and_sign(text_to_encrypt)
puts "output (base64) = #{output}"
output
end
encrypt_text("secret text")
When you run this, you'll get something like the following output:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==--80d091e8799776113b2c0efd1bf75b344bf39994
The last line (output of the encrypt_and_sign
method) is a combination of two parts separated by --
(see source):
- the encrypted message (Base64-encoded) and
- the message signature (Base64-encoded).
The signature is not important for encryption so let's take a look in the first part - let's decode it in Rails console:
> Base64.strict_decode64("SGRTUXYxRys1N1haVWNpVWxxWTdCMHlyMk15SnQ0dWFBOCt3Z0djWVdBZz0tLTkrd1hBNWJMVm9HcnptZ3loOG1mNHc9PQ==")
=> "HdSQv1G+57XZUciUlqY7B0yr2MyJt4uaA8+wgGcYWAg=--9+wXA5bLVoGrzmgyh8mf4w=="
You can see that the decoded message again consists of two Base64-encoded parts separated by --
(see source):
- the encrypted message itself
- the initialization vector used in the encryption
Rails message encryptor uses the aes-256-cbc
cipher by default (note that this has changed since Rails 5.2). This cipher needs an initialization vector, which is randomly generated by Rails and must be present in the encrypted output so that we can use it together with the key to decipher the message.
Moreover, Rails does not encrypt the input data as a simple plain text, but rather a serialized version of the data, using the Marshal
serializer by default (source). If we decrypted such serialized value with openssl, we would still get a slightly garbled (serialized) version of the initial plain text data. That's why it will be more appropriate to disable serialization while encrypting the data in Rails. This can be done by passing a parameter to the encryption method:
# crypt = ActiveSupport::MessageEncryptor.new(key)
crypt = ActiveSupport::MessageEncryptor.new(key, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
A re-run of the code yields output that is slightly shorter than the previous version, because the encrypted data has not been serialized now:
salt (hexa) = 73616c7473616c74
key (hexa) = 196827b250431e911310f5dbc82d395782837b7ae56230dce24e497cf07b6518
output (base64) = SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=--58bbaf983fd20459062df8b6c59eb470311cbca9
Finally, we must find out some info about the encryption key derivation procedure. The source tells us that the KeyGenerator uses the pbkdf2_hmac_sha1
algorithm with 2**16 = 65536
iterations to derive the key from the password / secret.
Anatomy of an openssl
encrypted message
Now, a similar investigation is needed on the openssl
side to learn the details of its decryption process. First, if you encrypt anything using the openssl enc
tool, you will find out that the output has a distinct format:
Salted__<salt><encrypted_message>
It begins with the Salted__
magic string, then followed by the salt (in hex form) and finally followed by the encrypted data. To be able to decrypt any data using this tool, we must get our encrypted data into the same format.
The openssl
tool uses the EVP_BytesToKey
(see source) to derive the key by default but can be configured to use the pbkdf2_hmac_sha1
algorithm using the -pbkdf2
and -md sha1
options. The number of iterations can be set using the -iter
option.
How to decrypt Rails-encrypted message in openssl
So, finally we have enough information to actually try to decrypt a Rails-encrypted message in openssl
.
First we must decode the first part of the Rails-encrypted output again to get the encrypted data and the initialization vector:
> Base64.strict_decode64("SUlIWFBjSXRUc0JodEMzLzhXckJzUT09LS1oZGtPV1ZRc2I5Wi8zOG01dFNOdVdBPT0=")
=> "IIHXPcItTsBhtC3/8WrBsQ==--hdkOWVQsb9Z/38m5tSNuWA=="
Now let's take the IV (the second part) and convert it to a hexa string form, as that is the form that openssl
needs:
> Base64.strict_decode64("hdkOWVQsb9Z/38m5tSNuWA==").unpack("H*").first
=> "85d90e59542c6fd67fdfc9b9b5236e58" # the initialization vector in hex form
Now we need to take the Rails-encrypted data and convert it to the format that openssl
will recognize, i.e. prepend it with the magic string and salt and Base64-encode it again:
> Base64.strict_encode64("Salted__" + "saltsalt" + Base64.strict_decode64("IIHXPcItTsBhtC3/8WrBsQ=="))
=> "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" # encrypted data suitable for openssl
Finally, we can construct the openssl
command to decrypt the data:
$ echo "U2FsdGVkX19zYWx0c2FsdCCB1z3CLU7AYbQt//FqwbE=" |
> openssl enc -aes-256-cbc -d -iv 85d90e59542c6fd67fdfc9b9b5236e58 \
> -pass pass:password -pbkdf2 -iter 65536 -md sha1 -a
secret text
And voilá, we successfully decrypted the initial message!
The openssl
parameters are as follows:
-aes-256-cbc
sets the same cipher as Rails uses for encryption-d
stands for decryption-iv
passes the initialization vector in the hex string form-pass pass:password
sets the password used to derive the encryption key to "password"-pbkdf2
and-md sha1
set the same key derivation algorithm as is used by Rails (pbkdf2_hmac_sha1
)-iter 65536
sets the same number of iterations for key derivation as was done in Rails-a
allows to work with Base64-encoded encrypted data - no need to handle raw bytes in files
By default openssl
reads from STDIN, so we simply pass the encrypted data (in proper format) to openssl
using echo.
debugging
In case you hit any problems when decrypting with openssl
, it is useful to add the -P
parameter to the command line, which outputs debugging info about the cipher / key parameters:
$ echo ... | openssl ... -P
salt=73616C7473616C74
key=196827B250431E911310F5DBC82D395782837B7AE56230DCE24E497CF07B6518
iv =85D90E59542C6FD67FDFC9B9B5236E58
The salt
, key
, and iv
values must correspond to the debugging values printed by the original code in the encrypt_text
method printed above. If they are different, you know you are doing something wrong...
Now, I guess you can expect similar problems when trying to decrypt the message in go but I think you have some good pointers now to start.
AES-256-CBC with Digest from Ruby to NodeJS
The critical missing piece is the IV, which is required when encryption/decryption is to be made across language boundaries as apparently the encrypter will generate a random IV (or something like that - still don't understand how Ruby decrypts the string without an IV.... but then what do I know....), if one is not provided.
The following snippets show how to encrypt a string in Ruby and decrypt in NodeJS.
#!/usr/bin/env ruby
require 'openssl'
require 'base64'
require 'openssl/cipher'
require 'openssl/digest'
aes = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
aes.encrypt
aes.key = Digest::SHA256.digest('IHazSekretKey')
aes.iv = '1234567890123456'
p Base64.encode64( aes.update('text to be encrypted') << aes.final )
The above prints: "eiLbdhFSFrDqvUJmjbUgwD8REjBRoRWWwHHImmMLNZA=\n"
#!/usr/bin/env node
var crypto = require('crypto');
function decrypto(toDecryptStr) {
var result,
encoded = new Buffer(toDecryptStr, 'base64'),
decodeKey = crypto.createHash('sha256').update('IHazSekretKey', 'ascii').digest(),
decipher = crypto.createDecipheriv('aes-256-cbc', decodeKey, '1234567890123456');
result = decipher.update(encoded);
result += decipher.final();
return result;
}
console.log(decrypto('eiLbdhFSFrDqvUJmjbUgwD8REjBRoRWWwHHImmMLNZA=\n'))
The JS script now properly decrypts the string.
One unfortunate side effect is that existing encrypted data will need to be decrypted and then re-encrypted with an IV that is then used in the decrypting implementation.
A PITA but nonetheless a working solution.
AES equivalent in Ruby openssl?
Digging into Gibberish code... provides the clues to the answers. and why the traditional mechanism does not work.
dec = function(string, pass) {
// string, password in plaintext
var cryptArr = Base64.decode(string),
salt = cryptArr.slice(8, 16),
pbe = openSSLKey(s2a(pass), salt),
key = pbe.key,
iv = pbe.iv;
cryptArr = cryptArr.slice(16, cryptArr.length);
// Take off the Salted__ffeeddcc
string = rawDecrypt(cryptArr, key, iv);
return string;
},
Converting to ruby is now fairly trivial.. noting it down for my personal future reference.
require 'base64'
require 'openssl'
def decode(k,t)
cryptArr = Base64.decode64(t)
salt = cryptArr[8..15]
data = cryptArr[16..-1]
aes = OpenSSL::Cipher::Cipher.new('AES-256-CBC').decrypt
aes.pkcs5_keyivgen(k, salt, 1)
s = aes.update(data) + aes.final
end
orig = "Made with Gibberish\n"
cipr = "U2FsdGVkX1+21O5RB08bavFTq7Yq/gChmXrO3f00tvJaT55A5pPvqw0zFVnHSW1o"
pass = "password"
puts decode(pass, cipr)
Openssl aes-256-cbc encryption from command prompt and decryption in PHP (and vice versa)
The OpenSSL statement generates a random 8 bytes salt during encryption, which is used together with the password to derive a 32 bytes key and a 16 bytes IV with the OpenSSL function EVP_BytesToKey()
.
With key and IV the encryption is performed with AES-256 in CBC mode. The result consists of the concatenation of the ASCII encoding of Salted__
, followed by the salt and the actual ciphertext, all Base64 encoded.
The decryption in PHP/OpenSSL must be implemented as follows:
- Determination of salt and actual ciphertext.
- Using salt, password and
EVP_BytesToKey()
to get key and IV. - Using key and IV to perform decryption with AES-256 in CBC mode.
One possible implementation is:
<?php
function EVP_BytesToKey($salt, $password) {
$bytes = '';
$last = '';
while(strlen($bytes) < 48) {
$last = hash('md5', $last . $password . $salt, true);
$bytes.= $last;
}
return $bytes;
}
$saltCiphertext = base64_decode('U2FsdGVkX18ruQUgA9LEOOvdOUQXv/o8z6ZNO820MKzSIbMjFcyfNo1efQwAOINxMY9+UxZjxaT+JEWmlUyYQw==');
$salt = substr($saltCiphertext, 8, 8);
$ciphertext = substr($saltCiphertext, 16);
$keyIv = EVP_BytesToKey($salt, 'sw8/M!CLl:=cmgtHts?v/Wb7C$Vk9Sy-{go.*+E;[GAg~KQi*rI!1#z;x/KT');
$key = substr($keyIv, 0, 32);
$iv = substr($keyIv, 32);
echo openssl_decrypt($ciphertext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); // {un:est@test.com,upass:klkKJS*dfd!j@d76w}
?>
In earlier versions OpenSSL used MD5 as digest in EVP_BytesToKey()
by default, from version V1.1.0 SHA256. In the posted example, decryption with MD5 is successful, so obviously MD5 was used in encryption.
Note that key derivation with EVP_BytesToKey()
is deemed insecure nowadays.
Decrypting a PHP generated OpenSSL string in Terminal
Just like your last question, you are struggling with encodings a bit. If you carefully consult the documentation for openssl_encrypt
, you'll note that both the key and IV should be passed as raw values, not hex.
You did this correctly in your code with the IV, but not the key. You passed the key as a hex value, which means it was twice as long as it needed to be. Just the first 256-bits of the key are used, in this case, B374A26A71490437AA024E4FADD5B497
, since you passed 512-bits of key material in total.
So we know that our raw key, when ASCII encoded, is B374A26A71490437AA024E4FADD5B497
, which is exactly 256-bits. However, the OpenSSL -K flag that I discussed in your first question requires the key to be passed hex encoded, which means we need to hex encode our key. So we hex encode B374A26A71490437AA024E4FADD5B497
to get 4233373441323641373134393034333741413032344534464144443542343937
, which is the actual hex encoded encryption key.
So, in summary, the final command is this, which gives an output of just the byte 0x70, which I assume is correct:
openssl enc -d -K 4233373441323641373134393034333741413032344534464144443542343937 -iv 61736466673534336173646667353433 -in input.bin -out out.bin
This assumes that input.bin is the base64 decoded binary of the base64 ciphertext you provided.
OpenSSL Encryption Key and IV differs from that generated from Java program
In addition to removing the CRLF from the password value as per comment by reinier:
You are apparently using OpenSSL 1.1.0 or higher, which changed the hash used by default in enc
PBKDF/PBE from MD5 to SHA256. See man enc
(may require a special section like 1ssl
) on a Unixy system which yours apparently isn't or on the web under HISTORY, and maybe Password to key function compatible with OpenSSL commands? . Note with SHA256 you need only 2 chunks not 4 in the PBKDF.
FYI you don't need to copy the array slices; both SecretKeySpec
and IvParameterSpec
have constructor overloads that use a slice of the byte array rather than the whole array. And AES-256 key is 32 bytes (at least in Java), which is what you coded even though you commented differently.
Related Topics
Activerecord::Connectiontimeouterror
Zlib in Ruby to Uncompress .Gz
Override Ruby Constant in Subclass So Inherited Methods Use New Constant Instead of the Old
Ruby Backslash to Continue String on a New Line
Ruby: Split String at Character, Counting from the Right Side
Use of Caret Symbol (^) in Ruby
Missing Host to Link To! Please Provide the :Host Parameter, for Rails 4
Rails Date Format in Form Field
Ruby Forgets Local Variables During a While Loop
Disable Sprockets Asset Caching in Development
How to Insert a String into a Textfile
How to Fire Raw Mongodb Queries Directly in Ruby
How to Install Redcloth on Windows
Has Anyone Successfully Deployed a Rails Project with Ruby 1.9.1
Ruby Hash Autovivification (Facets)
Ruby | Find a Way to Find an Exception on the Same Word to Capitalize