Reputation: 244
How do I create an encrypted file in Ruby with the following constraints?
Note: Question clarified after a misunderstanding.
Upvotes: 2
Views: 1359
Reputation: 35483
If you have GPG installed then there's a fast, easy, reliable way:
open("| gpg [options]","w"){|f| f.syswrite(data) }
Example:
require 'shellwords'
data = "Hello World"
password = "letmein"
gpg = "/usr/local/bin/gpg \
--symmetric \
--cipher-algo aes256 \
--digest-algo sha256 \
--cert-digest-algo sha256 \
--batch --yes \
--passphrase #{password.shellescape} \
--output /tmp/out.gpg
"
open("| #{gpg}","w"){|f| f.syswrite(data) }
In general, it's more secure to use the system's built-in GPG, rather than trying to manage your own crypto.
Upvotes: 3
Reputation: 244
Solved this without needing to spawn a process by interoperating with openssl:
Update: See my other answer to use gpg instead
@@OPENSSL_MAGIC = "Salted__"
@@DEFAULT_CIPHER = "aes-256-cbc"
@@DEFAULT_MD = OpenSSL::Digest::SHA256
# Note: OpenSSL "enc" uses a non-standard file format with a custom key
# derivation function and a fixed iteration count of 1, which some consider
# less secure than alternatives such as OpenPGP/GnuPG
#
# Resulting bytes when written to #{FILE} may be decrypted from the command
# line with `openssl enc -d -#{cipher} -md #{md} -in #{FILE}`
#
# Example:
# openssl enc -d -aes-256-cbc -md sha256 -in file.encrypted
def encrypt_for_openssl(
password,
data,
cipher = @@DEFAULT_CIPHER,
md = @@DEFAULT_MD.new
)
salt = SecureRandom.random_bytes(8)
cipher = OpenSSL::Cipher::Cipher.new(cipher)
cipher.encrypt
cipher.pkcs5_keyivgen(password, salt, 1, md)
encrypted_data = cipher.update(data) + cipher.final
@@OPENSSL_MAGIC + salt + encrypted_data
end
# Data may be written from the command line with
# `openssl enc -#{cipher} -md #{md} -in #{INFILE} -out #{OUTFILE}`
# and the resulting bytes may be read by this function.
#
# Example:
# openssl enc -aes-256-cbc -md sha256 -in file.txt -out file.txt.encrypted
def decrypt_from_openssl(
password,
data,
cipher = @@DEFAULT_CIPHER,
md = @@DEFAULT_MD.new
)
input_magic = data.slice!(0, 8)
input_salt = data.slice!(0, 8)
cipher = OpenSSL::Cipher::Cipher.new(cipher)
cipher.decrypt
cipher.pkcs5_keyivgen(password, input_salt, 1, md)
c.update(data) + c.final
end
This is based on the forge.js security library, specifically the example to match openssl's enc tool and using an iteration count of 1.
Upvotes: 0
Reputation: 244
Neither ruby-gpgme nor openpgp seem to work well, so spawning gpg seems like the best approach currently.
require "openssl"
require "digest/sha2"
require "open3"
require "tempfile"
....
@@OPENPGP_DEFAULT_CIPHER = "AES256"
@@DEFAULT_MD = OpenSSL::Digest::SHA512
# Return symmetrically encrypted bytes in RFC 4880 format which can be
# read by GnuPG or PGP. For example:
# $ gpg --decrypt file.pgp
#
# ruby-gpgme doesn't seem to work: https://github.com/ueno/ruby-gpgme/issues/11
# openpgp doesn't seem to work: https://github.com/bendiken/openpgp/issues/2
# Therefore we assume gpg is installed and try to spawn out to it
def encrypt_for_pgp(
password,
data,
cipher = @@OPENPGP_DEFAULT_CIPHER,
md = @@DEFAULT_MD.new
)
input_file = Tempfile.new('input')
begin
input_file.write(data)
input_file.close
output_file = Tempfile.new('output')
begin
Open3.popen3(
"gpg --batch --passphrase-fd 0 --yes --homedir /tmp/ " +
"--cipher-algo #{cipher} --s2k-digest-algo #{md.name} " +
"-o #{output_file.path} --symmetric #{input_file.path}"
) do |stdin, stdout, stderr, wait_thr|
stdin.write(password)
stdin.close_write
exit_status = wait_thr.value
if exit_status != 0
raise "Exit status " + exit_status.to_s
end
end
output_file.close
return IO.binread(output_file.path)
ensure
output_file.unlink
end
ensure
input_file.unlink
end
end
Upvotes: 0