June 3, 2017

Encrypted Directories

In the *nix ecosystem, there are a lot of tools that are great to chain together. I’d encourage everyone to learn their tools, but sometimes the tools are complex, and you only want to do something simple.

I keep forgetting how to crypto. What I generally want to do is encrypt and sign a directory, then pull that same directory later.

Here’s the script I threw together today. It takes one argument, either a directory to encrypt, or a gnupg encrypted tar gzipped directory and extracts it.

#!/usr/bin/env ruby

# gpg_folder - Script to quickly tar, compress and encrypt directories
# Copyright (C) 2017  Matthew B. Gray
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# GpgFolder service drives the OS to perform encrypt/decrypt methods
GpgFolder = Struct.new(:opts) do
  def call
    return false if opts.errors.any?

    if opts.encrypt?
      do_encrypt
    elsif opts.decrypt?
      do_decrypt
    else
      raise ";_; --not sure what I should do now"
    end

    true
  end

  def print_errors
    puts "Computer says no"
    errors.each do |error|
      puts "* #{error}"
    end
  end

  private

  def do_encrypt
    run("tar -cz #{opts.dir_name} | gpg --sign -o #{opts.archive_name}")
  end

  def do_decrypt
    run("gpg -d #{opts.archive_name} | tar xz")
  end

  def run(cmd)
    puts cmd
    system(cmd)
  end
end

# Interprets how the application was called, used to direct GpgFolder service
Options = Struct.new(:dir_or_file) do
  CRYPT_EXTENSION = /\.tgz.gpg\z/

  def errors
    @errors ||= [].tap do
      unless encrypt? ^ decrypt?
        errors << "Please specify a directory to encrypt, or a file ending in .tgz.gpg"
      end
    end
  end

  def encrypt?
    !dir_or_file.nil? && !decrypt? && File.directory?(dir_or_file)
  end

  def decrypt?
    !dir_or_file.nil? && dir_or_file.match(CRYPT_EXTENSION)
  end

  def dir_name
    if encrypt?
      dir_or_file
    else
      dir_or_file.sub(CRYPT_EXTENSION, '')
    end
  end

  def archive_name
    if encrypt?
      "#{dir_or_file}.tgz.gpg"
    else
      dir_or_file
    end
  end
end

opts = Options.new(ARGV.first)
service = GpgFolder.new(opts)
service.call or service.print_errors