DEV Community

Cover image for How to encrypt files with Ruby and Active Support
Steve Polito
Steve Polito

Posted on • Originally published at stevepolito.design

How to encrypt files with Ruby and Active Support

In this tutorial, we will create a Ruby binstub that allows you to encrypt and
decrypt files using Active Support's EncryptedFile module. This script is
particularly useful for those who need to protect sensitive information within
their files.

Use Cases

  • Encrypt text files in open source projects so that they can be committed to your repository without exposing sensitive information.
  • Storing credentials in an encrypted file, similar to Rails Custom Credentials.
  • Taking secure personal notes.

Required Libraries

The script starts with the following lines, which load the required libraries
and set up the command line argument handling.

#!/usr/bin/env ruby
require "active_support/encrypted_file"
require "securerandom"
Enter fullscreen mode Exit fullscreen mode

These lines import the necessary Ruby modules:
active_support/encrypted_file for encryption and decryption, and
securerandom for key generation

Parsing Arguments

The script expects one of three commands as the first argument: setup,
write, or read. If no command is provided, the script will print an error
message and exit.

command = ARGV.shift

if command.nil?
  puts "Please pass 'setup', 'write [FILE]', or 'read [FILE]'"
  exit 1
end
Enter fullscreen mode Exit fullscreen mode

Helper Methods

The following build_encrypted_file method creates an instance of
ActiveSupport::EncryptedFile with the necessary configuration options.

def build_encrypted_file(file)
  ActiveSupport::EncryptedFile.new(
    content_path: file,
    key_path: "invisible_ink.key",
    env_key: "INVISIBLE_INK_KEY",
    raise_if_missing_key: true
  )
end
Enter fullscreen mode Exit fullscreen mode

Next, we have two helper methods: handle_missing_key and
handle_missing_file_argument. These methods print error messages and exit the
script if a key or a file is missing, respectively.

Executing the Commands

The case statement is the main part of the script, handling the three possible
commands: write, read, and setup.

Encrypting a File

For the write command, the script ensures that a file argument is provided
and that a system editor is available to open the file. It then creates a new
encrypted file if it does not exist and opens the file in the system editor for
editing. When the editor is closed, the encrypted file is saved with the new
content.

when "write"
  file = ARGV.shift
  handle_missing_file_argument if file.nil?
  if ENV["EDITOR"].to_s.empty?
    puts "No $EDITOR to open file in"
    exit 1
  end
  begin
    encrypted_file = build_encrypted_file(file)
    encrypted_file.write(nil) unless File.exist?(file)
    encrypted_file.change do |tmp_path|
      system(ENV["EDITOR"], tmp_path.to_s)
    rescue Interrupt
      puts "File not saved"
    end
  rescue ActiveSupport::EncryptedFile::MissingKeyError => error
    handle_missing_key(error)
  end
Enter fullscreen mode Exit fullscreen mode

Decrypting a File

For the read command, the script ensures that a file argument is provided and
then attempts to read the encrypted file, printing the decrypted contents to the
standard output.

when "read"
  begin
    file = ARGV.shift
    handle_missing_file_argument if file.nil?
    encrypted_file = build_encrypted_file(file)
    puts encrypted_file.read
  rescue ActiveSupport::EncryptedFile::MissingKeyError => error
    handle_missing_key(error)
  end
Enter fullscreen mode Exit fullscreen mode

The Setup Script

Finally, the setup command generates a new encryption key and saves it to a
file named invisible_ink.key. It also adds the key file to the .gitignore
file to prevent it from being accidentally committed to a version control
system.

when "setup"
  if File.exist?("invisible_ink.key")
    puts "ERROR: invisible_ink.key already exists"
    exit 1
  else
    File.open(".gitignore", "a") { |file| file.puts("invisible_ink.key") }
    key = ActiveSupport::EncryptedFile.generate_key
    File.write("invisible_ink.key", key)
    puts "invisible_ink.key generated"
  end
Enter fullscreen mode Exit fullscreen mode

The Final Script

By using this script, you can protect sensitive information from unauthorized
access and maintain the privacy of your data. With the setup, write, and
read commands, you can easily manage the encryption and decryption process,
making it a useful tool for various applications.

As an alternative, you can also use the invisible_ink gem, which provides
the same functionality as this script, with the added benefit of being easily
integrated into any Ruby project. This gem offers a more streamlined and
maintainable approach to file encryption and decryption in your Ruby
applications, without the need to implement the entire script yourself.

#!/usr/bin/env ruby
require "active_support/encrypted_file"
require "securerandom"

# ℹ️ Set the command from the first argument
command = ARGV.shift

# ℹ️ Return early if no command was passed
if command.nil?
  puts "Please pass 'setup', 'write [FILE]', or 'read [FILE]'"
  exit 1
end

# ℹ️ Create an encrypted file instance.
def build_encrypted_file(file)
  ActiveSupport::EncryptedFile.new(
    content_path: file,
    # ℹ️ The key will be generated during the 'setup' script
    key_path: "invisible_ink.key",
    # ℹ️ Alternatively use an environment variable to store the key
    env_key: "INVISIBLE_INK_KEY",
    raise_if_missing_key: true
  )
end

# ℹ️ Handle case where ActiveSupport::EncryptedFile::MissingKeyError is raised
def handle_missing_key(error)
  puts "ERROR: #{error}"
  puts ""
  puts "Did you run 'setup'?"
  exit 1
end

# ℹ️ Handle case where a file was not passed as an argument
def handle_missing_file_argument
  puts "Please pass a file"
  exit 1
end

case command
when "write"
  # ℹ️ Set the file from the second argument
  file = ARGV.shift
  # ℹ️ Ensure the file was passed as an argument
  handle_missing_file_argument if file.nil?
  # ℹ️ Return early if there is no system editor to edit the file
  if ENV["EDITOR"].to_s.empty?
    puts "No $EDITOR to open file in"
    exit 1
  end
  begin
    encrypted_file = build_encrypted_file(file)
    # ℹ️ Writing a blank file creates the file in cases where it does not yet
    # exist. We need a file before we can write to it.
    encrypted_file.write(nil) unless File.exist?(file)
    # ℹ️ Open the file in the system $EDITOR using a temporary path
    encrypted_file.change do |tmp_path|
      system(ENV["EDITOR"], tmp_path.to_s)
    # ℹ️ Handle case where the $EDITOR is closed before the file was saved
    rescue Interrupt
      puts "File not saved"
    end
  # ℹ️ Print message to user if the key is missing
  rescue ActiveSupport::EncryptedFile::MissingKeyError => error
    handle_missing_key(error)
  end
when "read"
  begin
  # ℹ️ Set the file from the second argument
    file = ARGV.shift
  # ℹ️ Ensure the file was passed as an argument
    handle_missing_file_argument if file.nil?
    encrypted_file = build_encrypted_file(file)
    # ℹ️ Print decrypted file contents to $STDOUT
    puts encrypted_file.read
  # ℹ️ Print message to user if the key is missing
  rescue ActiveSupport::EncryptedFile::MissingKeyError => error
    handle_missing_key(error)
  end
when "setup"
  # ℹ️ Return early if there is already a key
  if File.exist?("invisible_ink.key")
    puts "ERROR: invisible_ink.key already exists"
    exit 1
  else
    # ℹ️ Prevent key from being saved to version control
    File.open(".gitignore", "a") { |file| file.puts("invisible_ink.key") }
    # ℹ️ Generate key
    key = ActiveSupport::EncryptedFile.generate_key
    # ℹ️ Write the key to a file
    File.write("invisible_ink.key", key)
    puts "invisible_ink.key generated"
  end
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)