Ruby: file encryption/decryption with private/public keys
Note Well: As emboss mentions in the comments, this answer is a poor fit for an actual system. Firstly, file encryption should not be carried out using this method (The lib provides AES, for example.). Secondly, this answer does not address any of the wider issues that will also affect how you engineer your solution.
The original source also goes into more details.
Ruby can use openssl to do this:
#!/usr/bin/env ruby
# ENCRYPT
require 'openssl'
require 'base64'
public_key_file = 'public.pem';
string = 'Hello World!';
public_key = OpenSSL::PKey::RSA.new(File.read(public_key_file))
encrypted_string = Base64.encode64(public_key.public_encrypt(string))
And decrypt:
#!/usr/bin/env ruby
# DECRYPT
require 'openssl'
require 'base64'
private_key_file = 'private.pem';
password = 'boost facile'
encrypted_string = %Q{
...
}
private_key = OpenSSL::PKey::RSA.new(File.read(private_key_file),password)
string = private_key.private_decrypt(Base64.decode64(encrypted_string))
from here
How to use PKI (public/private key) encryption in Ruby?
You are looking for built-in Ruby OpenSSL wrapper. The documentation provides examples of how to do it.
NOTE: Using .sign
method below to sign your data with a private key only generates a digital signature, it does not encrypt your data. From your question, it is not clear if you want to encrypt your data or just validate the message. If you want to encrypt the data, you will also have to use Cipher
class. You need only a digital signature to verify that your data has not been tempered with and been signed by you!
Sign your message
require 'openssl'
# Load PRIVATE key
private_key = OpenSSL::PKey::RSA.new(File.read('private_key.pem'))
# Sign your data
signature = private_key.sign(OpenSSL::Digest::SHA256.new, message)
# Our message signature that ensures that our data is signed by our private key
puts signature # => "\x04\xEC\xCC?\xDE\x8F\x91>G\xC2*M\xA7j\xA5\x16\..."
Now, send your data & signature to the receiving end. Also, you may consider using PKCS#7 as a standard way to pack your data and signature.
Verify your message & signature
require 'openssl'
# Load PUBLIC key
public_key = OpenSSL::PKey::RSA.new(File.read('public_key.pem'))
# We have received the following data
message = "Hello World!"
signature = "\x04\xEC\xCC?\xDE\x8F\x91>G\..." # Long signature
# Verify the message & its signature
if public_key.verify(OpenSSL::Digest::SHA256.new, signature, message)
"VALID: Signed by pair private key"
else
"NOT VALID: Data tampered or private-public key mismatch!"
end
Decrypt *.csv.gpg file using public / private key
Assuming you have a Gemfile
like:
source 'https://rubygems.org'
gem 'iostreams', '~> 0.14.0'
The following script will prompt you for the receiver's key ID and passphrase
require 'rubygems'
require 'bundler/setup'
require 'io/console'
require 'iostreams'
csv_filename = './data.csv'
encrypted_filename = './secure.pgp'
csv_data = File.read(csv_filename)
puts "Generating sender's key..."
signer_passphrase = 'somethingreallysecure'
sender_key_id = IOStreams::Pgp.generate_key(
name: 'Sender',
email: 'sender@example.org',
passphrase: signer_passphrase
)
puts 'Enter receiver key ID:'
receiver_key_id = gets.strip
puts "Downloading receiver's key..."
puts `gpg --keyserver keyserver.ubuntu.com --recv #{receiver_key_id}`
puts "Encrypting #{csv_filename} to #{encrypted_filename}"
sender_key = IOStreams::Pgp.list_keys(key_id: sender_key_id).first
receiver_key = IOStreams::Pgp.list_keys(key_id: receiver_key_id).first
IOStreams::Pgp::Writer.open(
'secure.pgp',
recipient: receiver_key[:email],
signer: sender_key[:email],
signer_passphrase: signer_passphrase
) do |output|
output.puts(csv_data)
end
puts "Decrypting #{encrypted_filename}"
puts 'Enter receiver passphrase:'
receiver_passphrase = STDIN.noecho(&:gets).chomp
decrypted_data = ''
IOStreams::Pgp::Reader.open('secure.pgp', passphrase: receiver_passphrase) do |stream|
decrypted_data += stream.read(10) until stream.eof?
end
puts ''
puts 'Source data'
puts '--------------'
puts csv_data
puts '--------------'
puts ''
puts 'Decrypted data'
puts '--------------'
puts decrypted_data
puts '--------------'
The bit you may have been missing is calling out to download ("receive") the key from the public server for the recipient.
Thanks to the RocketJob docs for some of the legwork here.
How to encrypt files with Ruby?
Ruby's OpenSSL is a thin wrapper around OpenSSL itself and provides almost all the functionality that OpenSSL itself does, so yes, there's a one-to-one mapping for all your examples:
openssl rand -base64 2048 > secret_key
That's actually exaggerated, you are using AES-256, so you only need a 256 bit key, you are not using RSA here. Ruby OpenSSL takes this decision off your shoulders, it will automatically determine the correct key size given the algorithm you want to use.
You are also making the mistake of using a deterministic IV during your encryption. Why? Because you don't specify an IV at all, OpenSSL itself will default to an IV of all zero bytes. That is not a good thing, so I'll show you the correct way to do it, for more information have a look at the Cipher documentation.
require 'openssl'
# encryption
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.encrypt
key = cipher.random_key
iv = cipher.random_iv
buf = ""
File.open("file.enc", "wb") do |outf|
File.open("file", "rb") do |inf|
while inf.read(4096, buf)
outf << cipher.update(buf)
end
outf << cipher.final
end
end
# decryption
cipher = OpenSSL::Cipher.new('aes-256-cbc')
cipher.decrypt
cipher.key = key
cipher.iv = iv # key and iv are the ones from above
buf = ""
File.open("file.dec", "wb") do |outf|
File.open("file.enc", "rb") do |inf|
while inf.read(4096, buf)
outf << cipher.update(buf)
end
outf << cipher.final
end
end
As you can see, encryption and decryption are fairly similar, so you can probably combine the streaming reading/writing into one shared method and just pass it a properly configured Cipher
plus the corresponding file names, I just stated them explicitly for the sake of clarity.
If you'd like to Base64-encode the key (and probably the IV, too), you can use the Base64 module:
base64_key = Base64.encode64(key)
Encrypting files with a decryption key
Using aes gem you can do something like:
key = AES.key
b64 = AES.encrypt("A super secret message", key)
AES.decrypt(b64, key) # => "A super secret message"
How to store the random key and iv created when trying to encrypt in ruby?
It looks like you're not experienced with developing encryption schemes, so I'm going to suggest you not implement this yourself. The built-in OpenSSL library does not protect you from making security mistakes. As you've noticed, it provides no guidance on how to securely handle the key and iv (and doing it wrong would be insecure).
I recommend using the rbnacl
gem, which has:
The
RbNaCl::SimpleBox
class provides a simple, easy-to-use cryptographic API where all of the hard decisions have been made for you in advance. If you're looking to encrypt something, and you don't know much about cryptography, this is probably what you want to be using.
Here's a full end-to-end example with outputting to & loading from disk:
require 'rbnacl'
generated_key = RbNaCl::Random.random_bytes(RbNaCl::SecretBox.key_bytes)
box = RbNaCl::SimpleBox.from_secret_key(generated_key)
ciphertext = box.encrypt('plaintext')
# save both artifacts to disk
File.open('cipher.bin', 'w') { |f| f.write(ciphertext) }
File.open('key.bin', 'w') { |f| f.write(generated_key) }
# decrypt by reading artifacts from disk
# this could be done in separate program
ciphertext_from_file = File.read('cipher.bin', mode: 'rb') # r -> "read", b -> "binary"
key_from_file = File.read('key.bin', mode: 'rb')
regenerated_box = RbNaCl::SimpleBox.from_secret_key(key_from_file)
regenerated_box.decrypt(ciphertext_from_file)
# => 'plaintext'
# cleanup
File.delete('cipher.bin') if File.exist?('cipher.bin')
File.delete('key.bin') if File.exist?('key.bin')
Note that generated_key
and ciphertext
are both binary strings. If you don't want to save raw binary files, then convert them using something like Base64.encode64
or Digest.hexencode
, converting them to printable characters. That way, they'll be printable, copy/paste-able, and you won't need to do a "read binary" with 'rb'
.
String encrypted in Ruby gives: 'BadPaddingException' when decrypted in Java
I wrote a very simple Java program to decrypt the output from that very simple Ruby program and it worked fine. Thus, not surprisingly, there's no inherent incompatibility between the ruby openssl module and standard Java cryptography. Without any more info about the Java side, all we can do is list some of the possibilities:
- Mismatched keys
The public key you are using must correspond to the private key the Java side is using. If not, you'll likely receive the BadPadding exception.
- Formatting issues
Obviously what you transmit, base64 encoded strings with embedded newlines, must be correctly parsed and decoded on the Java side. Getting a single byte wrong can cause the error you see. Most base64 decoders either choke on the newlines and throw an exception or ignore them. Neither explanation would result in a BadPaddingException. Perhaps the Java side is expecting Base64URL instead of Base64? If the Java side is expecting base64url encoding and if it ignores invalid characters then this could be the problem. It's worth trying to remove whitespace including newlines from the base64 output, and if that doesn't work then try encoding with a base64url encoder (again, remove any whitespace from the output).
Related Topics
Iterate Over a Deeply Nested Level of Hashes in Ruby
How to Use Bundler with Offline .Gem File
How to Install JSON Gem - Failed to Build Gem Native Extension(MAC 10.10)
Ruby: Why Does Puts Call To_Ary
How to Use Rspec's Should_Raise with Any Kind of Exception
How to Suppress Rails Console/Irb Outputs
Bundle Failing - Can't Find the Postgresql Client Library (Libpq)
Get Parent Directory of Current Directory in Ruby
Rails on Windows Is So Slow (Rails -V Takes 4 Seconds)
Ruby: How to Add "# Encoding: Utf-8" Automatically
Ruby on Rails - "Add 'Gem SQLite3'' to Your Gemfile"
Actionview::Template::Error (Incompatible Character Encodings: Utf-8 and Ascii-8Bit)
Why Doesn't Minitest::Spec Have a Wont_Raise Assertion
Why Does Ruby /[[:Punct:]]/ Miss Some Punctuation Characters
How to Run a Single Test in Minitest
How to Find Out Which Gem Has a Specific Dependency
Convert a .Doc or .Pdf to an Image and Display a Thumbnail in Ruby