In Ruby, how can I use OpenSSL::PKCS7's encrypt + decrypt functions? - ruby

Can anyone advise how to encrypt + decrypt a message in Ruby using OpenSSL::PKCS7? (I'm not trying to sign + verify. That much I can do. I need to write tests for some existing code which calls OpenSSL::PKCS7#decrypt.)
I've pored over what little documentation I can find online, but to no effect. When I call decrypt, I get the following on stderr:
example.rb:13:in `decrypt': decrypt error (OpenSSL::PKCS7::PKCS7Error)
require 'openssl'
# Create a key & self-signed certificate
key = OpenSSL::PKey::RSA.new 1024
cert = OpenSSL::X509::Certificate.new
cert.public_key = key.public_key
cert.not_before = Time.now
cert.not_after = Time.now + 2**12
cert.sign key, OpenSSL::Digest.new('SHA256')
# Encrypt & decrypt
ciphertext = OpenSSL::PKCS7.encrypt([cert], "plaintext").to_pem
decrypted = OpenSSL::PKCS7.new(ciphertext).decrypt(key, cert) # => error
I've furthermore experimented with specifying a cipher and flags for the OpenSSL::PKCS7::encrypt call, but to no effect.
I'm using Ruby 2.6.9.

Related

Decrypt hex strings using 3DES in Ruby from .NET

I'm working on a Ruby project that is interacting with a webservice that I'm exchanging some encrypted data with.
I am having a very hard time decrypting something I get back from the webservice in Ruby, although in the .NET side, it's working fine, and a number of other web-based or desktop-based tools can deal with this.
The encryption method was 3DES with ECB and no padding.
Below is a test script I have been working on. I've tried everything I can think of to get these strings unpacked correctly, but to no avail.
require 'openssl'
require 'base64'
def cipher(key, encrypted)
key = key.unpack('a2'*32).map{|x| x.hex}.pack('c'*32)
encrypted = encrypted.unpack('a2'*32).map{|x| x.hex}.pack('c'*32)
OpenSSL::Cipher::ciphers.select{|c| c.include? 'des3' }.map do |cipher_name|
begin
cipher = OpenSSL::Cipher.new(cipher_name)
cipher.padding = 0
cipher.decrypt
cipher.key=key
plain = cipher.update(encrypted) + cipher.final
p "Cipher #{cipher_name} success: #{plain} #{plain.class} #{plain.length} #{plain.encoding.to_s}"
plain
rescue => e
p "Cipher #{cipher_name} failed #{e}"
nil
end
end
end
key = '202FA9B21843D7022B6466DB68327E1F'
encrypted = 'ff6f07e270ebd5c0878c67c999d87ebf'
res1 = cipher key, encrypted
key = '49CE85147B24123718AB3F4539AB1A21'
encrypted = '995604ed8016da8897f1875ebd725529'
res2 = cipher key, encrypted
p res1 == res2 ? "SUCCESS" : "FAIL"
# In both cases, the correct output should be '25588015543912470222703296730936'
A 3DES key is 24-bytes, use a full length key.
3DES uses triple encryption with essentially a 24-byte key. 202FA9B21843D7022B6466DB68327E1F is hex encoded 16-byte key.
Try repeating the first 8-bytes of the key:
202FA9B21843D7022B6466DB68327E1F202FA9B21843D702
Some 3DES implementations will repeat 8-bytes of a 16-byte key but relying on such implementation details is not a good idea.
Note: 3DES actually uses a 168-bit key because the LSb of each byte is not used. Further because there are actually three DES calls the security is only 112-bits. Additionally DES has some weak keys. There are two common modes, ede and ded, in an effort to facilitate moving from DES to 3DES thus adding more confusion.
Finally: Move from 3DES to AES in CBC mode with a random IV. Please don't continue poor security practices.

AES-128-GCM does not seem to check for authentication

If I understand GCM mode correctly, it is supposed to provide not only encryption, but also authentication of the ciphertext. However when I use Ruby's OpenSSL implementation to encrypt data with AES-256-GCM mode, it will happily decrypt the data even if I tamper with the auth_tag. Am I missing something here or is the implementation indeed broken?
require 'openssl'
# ALICE encrypts some secret data
data = 'secret'
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
cipher.auth_data = 'auth_data'
ciphertext = cipher.update(data) + cipher.final
auth_tag = cipher.auth_tag
# EVE tampers with the auth tag, e.g. dropping the last 10 bytes
auth_tag = auth_tag[0..-11]
# BOB decrypts the ciphertext
cipher = OpenSSL::Cipher.new('aes-128-gcm')
cipher.decrypt
cipher.key = key
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.auth_data = 'auth_data'
data = cipher.update(ciphertext) + cipher.final
# BOB is very sad because no error is raised!
I am using OS X built in OpenSSL version:
% openssl version
OpenSSL 0.9.8zg 14 July 2015
GCM supports multiple sizes for the authentication tag. The authentication tag is shortened in these versions by removing bytes from the right. That's exactly what your attacker seems to be doing.
Now the API is dangerously unstable. First of all, (16 - 10) * 8 = 48, which is not a valid size for AES-GCM (according to NIST SP 800-38D. Furthermore, the authentication tag size should be an input or configuration parameter of the cipher. At the very least the API should warn users to check the authentication tag size themselves instead of allowing just any authentication tag input.
So yes, you are missing something and yes, I would say the implementation - or at the very least the documentation - is broken; good catch.

As a NodeJS user, how do I use the ruby OpenSSL library for crypto?

I'm Node developer, but every once in awhile I get to play around with ERB templates. I really love pulling out as much ruby as I can in these templates when I can and this idea caught my eye especially.
I have this configuration value, which should be encrypted, but is coming in plain text. The program would decrypt it like so:
var crypto = require('crypto');
var decipher = crypto.createDecipher('aes256', 'e20jhciwjf90u2r9u9ujj');
var decrypted = crypto.update('4ufujj90u19ru90u109u') + crypto.final();
I was wondering how I might go about creating an encrypted string for the above to decrypt using ruby?
So far I have:
require 'openssl'
cipher = OpenSSL::Cipher::Cipher.new('aes256');
cipher.key= 'e20jhciwjf90u2r9u9ujj'
encrypted = cipher.update('my cat is yellow and very pretty.') + cipher.final
Two problems I have:
I often get a Key length not long enough error on the ruby side.
Ruby outputs a bunch of crazy hex, whereas node seems to always take/want utf8.
Am I encrypting/decrypting safely?
Is there a way to universally translate/work laterally with these two APIs?
AES-256 uses a key of 256bits, and by default ruby uses utf-8 encoding, so each ansi character is 8bit long. So the key string must be 32 bytes.
Explicitly use an aes mode (e.g. aes-256-cbc).
Set the same iv(initial vector) on both sides
I finally succeeded with the above methods.
Here is my code:
Ruby side:
require 'openssl'
require 'base64'
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.encrypt
cipher.iv = 'a'*16;
cipher.key = '01234567890123456789012345678901' # should be 32 characters, 32*8=256 bits
enc = Base64.strict_encode64(cipher.update('01234567890123456789012345678901') + cipher.final)
puts enc
Javascript side:
var encrypted = new Buffer(base64Data, 'base64');
var crypto = require('crypto');
var decipher = crypto.createDecipheriv('aes-256-cbc', '01234567890123456789012345678901', 'aaaaaaaaaaaaaaaa');
var dec = decipher.update(encrypted);
console.log(Buffer.concat([dec, decipher.final()]));

Decrypting salted AES file generated on command line with Ruby

I would like to decrypt a text file within a ruby 2.1 script which was previously encrypted using OpenSSL's commandline tools:
openssl enc -aes-256-cbc -a -salt -in my_file
As seen in the command, the file is AES-256-CBC encrypted, salted and base64 encoded.
The password is known, but not the IV nor the key, which are required to follow this code snippet, taken from the ruby documentation:
decipher = OpenSSL::Cipher::AES.new 256, :CBC
decipher.decrypt
decipher.key = key
decipher.iv = iv
plain = decipher.update(encrypted_text) + decipher.final
While trying to find an answer, I found the gem AESCrypt gem which supposedly simplifies en- and decrypting, yet the currently released version is not compatible with ruby 2.1.
Looking at it's source code, I found that the key was retrieved by digesting the password, and the IV is just left as nil.
So I tried to get the following running:
encoded_and_encrypted_text = File.read my_file_path
encrypted_text = Base64.decode64 encoded_and_encrypted_text.to_s.strip
decipher = OpenSSL::Cipher::AES.new 256, :CBC
decipher.decrypt
decipher.key = OpenSSL::Digest::SHA256.new(my_password).digest
plain_text = decipher.update(encrypted_text) + decipher.final
But this results in OpenSSL::Cipher::CipherError: bad decrypt.
Do I need to somehow specifically handle that the file is salted? I have read in the OpenSSL documentation for the enc function that the IV, if not specified while encrypting the file, is generated from the password. Do I need to manually reconstruct the IV somehow?
Any advice would be highly appreciated :)
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?

While trying to setup an interoperable encryption system, I met a weird situation during a light "proof-of-concept".
I wrote the following code in Ruby to:
create an encrypted file from a dummy text file on my file system
decrypt the encrypted file
compare with the original file and check if they are the same
Here is the code:
require 'openssl'
require 'base64'
# Read the dummy file
data = File.read("test.txt")
# Create an encrypter
cipher = OpenSSL::Cipher::AES.new(256, :CBC)
cipher.encrypt
key = "somethingreallyreallycomplicated"
cipher.key = key
# Encrypt and save to a file
encrypted = cipher.update(data) + cipher.final
open "encrypted.txt", "w" do |io| io.write Base64.encode64(encrypted) end
# Create a decrypter
decipher = OpenSSL::Cipher::AES.new(256, :CBC)
decipher.decrypt
decipher.key = key
# Decrypt and save to a file
encrypted_data = Base64.decode64(File.read("encrypted.txt"))
plain = decipher.update(encrypted_data) + decipher.final
open "decrypted.txt", "w" do |io| io.write plain end
# Compare original message and decrypted message
puts data == plain #=> true
Everything works fine, this script outputs "true"
Then I tried to use the openssl command-line to decrypt my file with the following command:
openssl aes-256-cbc -d -a -in encrypted.txt -k somethingreallyreallycomplicated
But I got: bad magic number
How come?
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.

Resources