Kevin
Kevin

Reputation: 244

How do I encrypt a file with Ruby using symmetric AES256 that can be decrypted with gpg?

How do I create an encrypted file in Ruby with the following constraints?

  1. Data is an array of bytes supplied by an untrusted user.
  2. Password is an array of bytes supplied by an untrusted user.
  3. Password accepts all frequently used password characters, including password special characters (e.g. ', ", etc.).
  4. The file format should be a commonly used format such as OpenPGP.
  5. The encrypted file is sent back to the user.

Note: Question clarified after a misunderstanding.

Upvotes: 2

Views: 1359

Answers (3)

joelparkerhenderson
joelparkerhenderson

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

Kevin
Kevin

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

Kevin
Kevin

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

Related Questions