What is the ruby (cypher / pbkdf2_hmac incantations of the openssl gem) equivalent to encrypting with a command like this:
echo 'Top secret text' | openssl enc -base64 -e -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2 -p
I'm trying to use the ruby openssl gem to do AES-256-CBC encryption of a plain text string. I found quite a few examples of how to do this, but then I want to be able to give my friend the unix openssl command necessary to decrypt it at the other end, which means I need to use the same settings and string concatenation approach that the unix command does.
I figured out a pair of unix commands to encrypt and then decrypt. I'm using PBKDF2 key derivation. I have wrapped these commands in ruby code doing system call-outs, so my encrypt_openssl_system_call is successfully the inverse of decrypt.
I want my open_ssl_gem_encrypt method to also be decryptable using decrypt... and at the moment it nearly works because I've cheated and passed in the @salt_used, @key_used, and @iv_used. Instead I will need to figure out the right way to call OpenSSL::KDF.pbkdf2_hmac (or similar?) to do key derivation.
But with matching those values I suppose I hoped to see it encrypt to the exact same cypher text, or at least come out with the right plain text. Weirdly it nearly works, producing the plain text but with 14 random characters at the start.
require 'openssl'
require 'base64'
def password
"mypassword"
end
def encrypt_openssl_system_call(plain_text)
command = "echo '#{plain_text}' | openssl enc -base64 -e -aes-256-cbc -salt -pass pass:'#{password}' -pbkdf2 -p"
puts command
output = `#{command}`
puts output
raise(output) unless $?.success?
# Parse the actual used salt key and iv from this output
rows = output.split("\n")
@salt_used = [rows[0].split("salt=").last].pack('H*')
@key_used = [rows[1].split("key=").last].pack('H*')
@iv_used = [rows[2].split("iv =").last].pack('H*')
encrypted = rows.last
encrypted.rstrip!
encrypted
end
def decrypt(encrypted)
command = "echo '#{encrypted}' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'#{password}' -pbkdf2"
puts command
output = `#{command}`
raise output unless $?.success?
output.rstrip
rescue RuntimeError => e
puts ">>> ERROR #{e.message}"
puts e.backtrace
e.message
end
def open_ssl_gem_encrypt(plain_text)
# Key derivation (PBKDF2) not working yet
# salt = 'Salted__' + OpenSSL::Random.random_bytes(8)
# key = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10000, length: 32, hash: "sha1")
salt = 'Salted__' + @salt_used
key = @key_used
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
iv = @iv_used # cipher.random_iv
cipher.iv = iv
# Concatenate bits in hopefully the right order?!
encrypted = salt
encrypted << iv
encrypted << cipher.update(plain_text)
encrypted << cipher.final
Base64.encode64(encrypted).gsub(/\n/, '')
end
PLAIN_TEXT = "Top secret text"
puts "encrypt_openssl_system_call(#{PLAIN_TEXT.dump})"
encrypted = encrypt_openssl_system_call(PLAIN_TEXT)
puts "openssl command produced '#{encrypted}'"
puts "\n"
puts "decrypt('#{encrypted}')"
decrypted = decrypt(encrypted)
puts "openssl command decryption produced '#{decrypted}'"
puts "So far so good!" if decrypted==PLAIN_TEXT
puts "\n"
puts "open_ssl_gem_encrypt(#{PLAIN_TEXT.dump})"
encrypted_b = open_ssl_gem_encrypt(PLAIN_TEXT)
puts "OpenSSL gem produced '#{encrypted_b}'"
puts "\n"
puts "decrypt(#{encrypted_b.dump})"
decrypted_b = decrypt(encrypted_b)
puts "openssl command decryption produced '#{decrypted_b}'"
Output:
encrypt_openssl_system_call("Top secret text")
echo 'Top secret text' | openssl enc -base64 -e -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2 -p
salt=27BD7552C3308BBA
key=B99FCDC5F9296AD2B1488E49B8CD29EDF0D15E13C408B1EEB11A2050F6403E94
iv =1294745B0A06C42939283E51EAE29E5E
U2FsdGVkX18nvXVSwzCLuhZeGj9c+fKLG+nDaitgeRahxKftT20Rax2sYFpjiO3h
openssl command produced 'U2FsdGVkX18nvXVSwzCLuhZeGj9c+fKLG+nDaitgeRahxKftT20Rax2sYFpjiO3h'
decrypt('U2FsdGVkX18nvXVSwzCLuhZeGj9c+fKLG+nDaitgeRahxKftT20Rax2sYFpjiO3h')
echo 'U2FsdGVkX18nvXVSwzCLuhZeGj9c+fKLG+nDaitgeRahxKftT20Rax2sYFpjiO3h' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
So far so good!
open_ssl_gem_encrypt("Top secret text")
OpenSSL gem produced 'U2FsdGVkX18nvXVSwzCLuhKUdFsKBsQpOSg+Uerinl4DIK8yljJ2aHCR+8m9Yrq3'
decrypt("U2FsdGVkX18nvXVSwzCLuhKUdFsKBsQpOSg+Uerinl4DIK8yljJ2aHCR+8m9Yrq3")
echo 'U2FsdGVkX18nvXVSwzCLuhKUdFsKBsQpOSg+Uerinl4DIK8yljJ2aHCR+8m9Yrq3' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced ' |�S��p��C��H�PTop secret text'
Partial success shows that I must surely be putting the string together in the right order at least!?
openssl gem version is 2.1.2
openssl version is "LibreSSL 3.3.6"
OpenSSL, when using a password/salt, stores the result as a concatenation of the ASCII encoding of Salted__, followed by the 8 bytes salt and the actual ciphertext (the -base64 option additionally performs a Base64 encoding).
In the current code, open_ssl_gem_encrypt() additionally writes the IV after the salt, which is incorrect. To fix this, the line encrypted << iv has to be removed. Then the output is:
encrypt_openssl_system_call("Top secret text")
echo 'Top secret text' | openssl enc -base64 -e -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2 -p
salt=6C70B083F3E5820D
key=091EB1C0043A17F1CB7932023BEBED42E36A8EB05709A93B8A35CBD02CAAEFEF
iv =C7D740EF0B0AB71E45C21A95C4004ADC
U2FsdGVkX19scLCD8+WCDRSr0MrSj82D5EOvO4nmG0jkd4KVyApoO2mIB1Tn7B8v
openssl command produced 'U2FsdGVkX19scLCD8+WCDRSr0MrSj82D5EOvO4nmG0jkd4KVyApoO2mIB1Tn7B8v'
decrypt('U2FsdGVkX19scLCD8+WCDRSr0MrSj82D5EOvO4nmG0jkd4KVyApoO2mIB1Tn7B8v')
echo 'U2FsdGVkX19scLCD8+WCDRSr0MrSj82D5EOvO4nmG0jkd4KVyApoO2mIB1Tn7B8v' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
So far so good!
open_ssl_gem_encrypt("Top secret text")
OpenSSL gem produced 'U2FsdGVkX19scLCD8+WCDUflsXl8c0g44eFNBNy3S5Q='
decrypt("U2FsdGVkX19scLCD8+WCDUflsXl8c0g44eFNBNy3S5Q=")
echo 'U2FsdGVkX19scLCD8+WCDUflsXl8c0g44eFNBNy3S5Q=' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
As can be seen, the decryption of the ciphertext generated with open_ssl_gem_encrypt() now works!
However, the ciphertexts generated with encrypt_openssl_system_call() and open_ssl_gem_encrypt() are different, which should not be, since your code applies the same salt in both cases.
The reason is that in encrypt_openssl_system_call() a line break is produced, which can be prevented by using -n (s. here). If -n is used, the output is:
encrypt_openssl_system_call("Top secret text")
echo -n 'Top secret text' | openssl enc -base64 -e -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2 -p
salt=F7083C17A99C44C2
key=E424DA2D1AD290B3FA89829495C3F04898150E1C722B2B86159CE10610553BB7
iv =F0ACA075C63C8D8D80F66137645F8333
U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw=
openssl command produced 'U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw='
decrypt('U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw=')
echo 'U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw=' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
So far so good!
open_ssl_gem_encrypt("Top secret text")
OpenSSL gem produced 'U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw='
decrypt("U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw=")
echo 'U2FsdGVkX1/3CDwXqZxEwuInAQktjTeuW7TRmwETBgw=' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
Now, with identical salt, the two ciphertexts produced are the same!
The common implementation for the encryption using a salt/password is to first generate a random 8 bytes salt (as may have been attempted in the commented-out part), and from that, together with password and key derivation function, the key and IV are calculated.
With the key and IV derived this way, the encryption is then performed. A possible implementation is:
def open_ssl_gem_encrypt_v2(plain_text)
# Key derivation (PBKDF2)
salt = OpenSSL::Random.random_bytes(8)
keyIv = OpenSSL::KDF.pbkdf2_hmac(password, salt: salt, iterations: 10000, length: 32+16, hash: "sha256")
key = keyIv[0..31]
iv = keyIv[32..-1]
# Encrypt
cipher = OpenSSL::Cipher.new('AES-256-CBC')
cipher.encrypt
cipher.key = key
cipher.iv = iv
ciphertext = cipher.update(plain_text) + cipher.final
# Concatenate bits
encrypted = 'Salted__' + salt + ciphertext
Base64.encode64(encrypted).gsub(/\n/, '')
end
If this implementation is used instead of the old one, the last part of the output is e.g.:
...
So far so good!
open_ssl_gem_encrypt("Top secret text")
OpenSSL gem produced 'U2FsdGVkX18KvUdJOd/Pz0Sf0FmNMA2HzQWeXkR63Y8='
decrypt("U2FsdGVkX18KvUdJOd/Pz0Sf0FmNMA2HzQWeXkR63Y8=")
echo 'U2FsdGVkX18KvUdJOd/Pz0Sf0FmNMA2HzQWeXkR63Y8=' | openssl enc -base64 -d -aes-256-cbc -salt -pass pass:'mypassword' -pbkdf2
openssl command decryption produced 'Top secret text'
As can be seen, the ciphertext is successfully decrypted using OpenSSL.
Note that OpenSSL defaults to MD5 as digest for the key derivation function in earlier versions and SHA256 as of version v1.1.0.
For decryption to be successful, the digest used in the key derivation function KDF.pbkdf2_hmac() must match the digest of the OpenSSL version used for decryption.
The digest can be explicitly set in the OpenSSL statement with the -md option so that you are not limited to the default digest, s. openssl enc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With