├── .gitignore ├── test ├── test_helper.rb ├── test-key.pub ├── test-key.sec └── gpg_helper_test.rb ├── .hgignore ├── .travis.yml ├── Gemfile ├── lib ├── rgpg.rb └── rgpg │ ├── gem_info.rb │ └── gpg_helper.rb ├── Rakefile ├── rgpg.gemspec ├── CHANGELOG.md ├── LICENSE ├── README.md └── bin └── rgpg /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | *~ 4 | scratch/ 5 | 6 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'rgpg' 3 | 4 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | Gemfile.lock 3 | *.gem 4 | *~ 5 | scratch/ 6 | 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.8.7 4 | - 1.9.3 5 | - 2.0.0 6 | 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org/' 2 | 3 | gemspec 4 | 5 | gem 'rake' 6 | 7 | -------------------------------------------------------------------------------- /test/test-key.pub: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workato/rgpg/master/test/test-key.pub -------------------------------------------------------------------------------- /test/test-key.sec: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/workato/rgpg/master/test/test-key.sec -------------------------------------------------------------------------------- /lib/rgpg.rb: -------------------------------------------------------------------------------- 1 | if RUBY_VERSION < '1.9.0' 2 | require File.expand_path('../rgpg/gem_info', __FILE__) 3 | require File.expand_path('../rgpg/gpg_helper', __FILE__) 4 | else 5 | require_relative 'rgpg/gem_info' 6 | require_relative 'rgpg/gpg_helper' 7 | end 8 | 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake' 3 | require 'rake/testtask' 4 | 5 | task :default => [:test, :build] 6 | 7 | Rake::TestTask.new(:test) do |t| 8 | t.libs << 'lib' 9 | t.pattern = 'test/**/*_test.rb' 10 | t.verbose = true 11 | end 12 | 13 | -------------------------------------------------------------------------------- /lib/rgpg/gem_info.rb: -------------------------------------------------------------------------------- 1 | module Rgpg 2 | module GemInfo 3 | MAJOR_VERSION = 0 4 | MINOR_VERSION = 4 5 | PATCH_VERSION = 0 6 | 7 | def self.version_string 8 | [MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION].join('.') 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /rgpg.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $:.unshift(lib) unless $:.include?(lib) 3 | 4 | require 'rgpg/gem_info' 5 | 6 | Gem::Specification.new do |s| 7 | s.name = 'rgpg' 8 | s.version = Rgpg::GemInfo.version_string 9 | s.date = Date.today rescue '1970-01-01' 10 | s.executables << 'rgpg' 11 | s.summary = 'rgpg' 12 | s.description = 'Simple Ruby wrapper around "gpg" command for file encryption' 13 | s.license = 'MIT' 14 | s.authors = 'Richard Cook' 15 | s.email = 'rcook@rcook.org' 16 | s.files = ['LICENSE'] + Dir.glob('lib/**/*.rb') 17 | s.require_paths = ['lib'] 18 | s.homepage = 'https://github.com/rcook/rgpg/' 19 | end 20 | 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | v0.4.0 4 | * Adds --no-tty to helper to allow use in rake tasks (mark-ellul) 5 | * Fixes error message (eahanson) 6 | 7 | v0.3.0 8 | * Adds passphrase support (mgreensmith) 9 | * Adds RSA support (mgreensmith) 10 | 11 | v0.2.4 12 | * Creates temporary gpg home directory for each command invocation 13 | * Wraps all invocations of gpg correctly 14 | * Fixes Ruby 1.8.7 compatibility issues 15 | 16 | v0.2.3 17 | * Sanitizes user input 18 | 19 | v0.2.2 (released) 20 | * Suppresses standard output from gpg commands 21 | 22 | v0.2.1 23 | * Many fixes 24 | * Many small improvements 25 | 26 | v0.2.0 27 | * Rewrite from scratch 28 | 29 | v0.1.0 30 | * Adds command-line driver 31 | 32 | v0.0.0 33 | * Initial version 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Richard Cook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rgpg [![Install gem](https://badge.fury.io/rb/rgpg.png)](https://rubygems.org/gems/rgpg) [![Build status](https://travis-ci.org/rcook/rgpg.png?branch=master,0.3.0,0.2.4)](https://travis-ci.org/rcook/rgpg) 2 | 3 | A simple Ruby wrapper around `gpg` command for file encryption 4 | 5 | _rgpg_ is a simple API for interacting with the `gpg` tool. It is specifically designed to avoid altering global keyring state by creating temporary public and secret keyrings on the fly for encryption and decryption. 6 | 7 | # Installation 8 | 9 | ```bash 10 | gem install rgpg 11 | ``` 12 | 13 | # Usage from terminal 14 | 15 | This gem adds an `rgpg` command. Type `rgpg` for usage information. 16 | 17 | # API usage 18 | 19 | ## To generate a GPG public-private key pair 20 | 21 | ```ruby 22 | require 'rgpg' 23 | 24 | Rgpg::GpgHelper.generate_key_pair 'mykey', 'me@example.com', 'Joe Bloggs' 25 | ``` 26 | 27 | ## To encrypt a file 28 | 29 | ```ruby 30 | require 'rgpg' 31 | 32 | Rgpg::GpgHelper.encrypt_file 'mykey.pub', 'myfile.txt', 'myfile.txt.enc' 33 | ``` 34 | 35 | ## To decrypt a file 36 | 37 | ```ruby 38 | require 'rgpg' 39 | 40 | Rgpg::GpgHelper.decrypt_file 'mykey.pub', 'mykey.sec', 'myfile.txt.enc', 'myfile.txt' [, 'secret_key_passphrase'] 41 | ``` 42 | 43 | # Licence 44 | 45 | _rgpg_ is released under the MIT licence. 46 | 47 | -------------------------------------------------------------------------------- /test/gpg_helper_test.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | if RUBY_VERSION < '1.9.0' 3 | require File.expand_path('../test_helper', __FILE__) 4 | else 5 | require_relative 'test_helper' 6 | end 7 | 8 | module RgpgTest 9 | class GpgHelperTest < Test::Unit::TestCase 10 | def test_encrypt_and_decrypt 11 | public_key_file_name = File.expand_path('../test-key.pub', __FILE__) 12 | private_key_file_name = File.expand_path('../test-key.sec', __FILE__) 13 | test_string = SecureRandom.base64 14 | 15 | self.class.with_temp_files(3) do |input_file, encrypted_file, decrypted_file| 16 | File.open(input_file.path, 'w') do |f| 17 | f.write(test_string) 18 | end 19 | 20 | Rgpg::GpgHelper.encrypt_file( 21 | public_key_file_name, 22 | input_file.path, 23 | encrypted_file.path 24 | ) 25 | Rgpg::GpgHelper.decrypt_file( 26 | public_key_file_name, 27 | private_key_file_name, 28 | encrypted_file.path, 29 | decrypted_file.path 30 | ) 31 | 32 | assert_equal test_string, File.read(decrypted_file.path) 33 | end 34 | end 35 | 36 | private 37 | 38 | def self.with_temp_files(count) 39 | begin 40 | temp_files = [] 41 | count.times do |index| 42 | temp_file = Tempfile.new("rgpg-test-gpg-helper-temp-#{index}") 43 | temp_files << temp_file 44 | temp_file.close 45 | end 46 | 47 | yield temp_files 48 | 49 | ensure 50 | while temp_files.size > 0 51 | temp_file = temp_files.pop 52 | temp_file.close 53 | temp_file.unlink 54 | end 55 | end 56 | end 57 | end 58 | end 59 | 60 | -------------------------------------------------------------------------------- /bin/rgpg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | require 'tempfile' 5 | require 'rgpg' 6 | 7 | THIS_BASE_NAME = File.basename($0, File.extname($0)) 8 | GENERATE_KEY_PAIR_USAGE = '--generate-key-pair ' 9 | ENCRYPT_USAGE = '--encrypt ' 10 | DECRYPT_USAGE = '--decrypt ' 11 | 12 | if ARGV[0] == '--generate-key-pair' 13 | raise RuntimeError.new(GENERATE_KEY_PAIR_USAGE) unless ARGV.size == 4 14 | key_base_name = ARGV[1] 15 | recipient = ARGV[2] 16 | real_name = ARGV[3] 17 | Rgpg::GpgHelper.generate_key_pair(key_base_name, recipient, real_name) 18 | exit 0 19 | elsif ARGV[0] == '--encrypt' 20 | raise RuntimeError.new(ENCRYPT_USAGE) unless ARGV.size == 4 21 | public_key_file_name = ARGV[1] 22 | input_file_name = ARGV[2] 23 | output_file_name = ARGV[3] 24 | Rgpg::GpgHelper.encrypt_file(public_key_file_name, input_file_name, output_file_name) 25 | exit 0 26 | elsif ARGV[0] == '--decrypt' 27 | raise RuntimeError.new(DECRYPT_USAGE) unless ARGV.size == 5 || ARGV.size == 6 28 | public_key_file_name = ARGV[1] 29 | private_key_file_name = ARGV[2] 30 | input_file_name = ARGV[3] 31 | output_file_name = ARGV[4] 32 | ARGV.size == 6 ? passphrase = ARGV[5] : passphrase = nil 33 | Rgpg::GpgHelper.decrypt_file(public_key_file_name, private_key_file_name, input_file_name, output_file_name, passphrase) 34 | exit 0 35 | else 36 | $stderr.puts "Unsupported command \"#{ARGV[0]}\"" unless ARGV[0].nil? || ARGV[0].size == 0 37 | $stderr.puts 'Usage:' 38 | $stderr.puts " #{THIS_BASE_NAME} ... " 39 | $stderr.puts 'Available commands:' 40 | $stderr.puts " #{GENERATE_KEY_PAIR_USAGE}" 41 | $stderr.puts " #{ENCRYPT_USAGE}" 42 | $stderr.puts " #{DECRYPT_USAGE}" 43 | exit 1 44 | end 45 | 46 | -------------------------------------------------------------------------------- /lib/rgpg/gpg_helper.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | require 'shellwords' 3 | 4 | module Rgpg 5 | module GpgHelper 6 | def self.generate_key_pair(key_base_name, recipient, real_name) 7 | public_key_file_name = "#{key_base_name}.pub" 8 | private_key_file_name = "#{key_base_name}.sec" 9 | script = generate_key_script(public_key_file_name, private_key_file_name, recipient, real_name) 10 | script_file = Tempfile.new('gpg-script') 11 | begin 12 | script_file.write(script) 13 | script_file.close 14 | run_gpg_no_capture( 15 | '--batch', 16 | '--gen-key', script_file.path 17 | ) 18 | ensure 19 | script_file.close 20 | script_file.unlink 21 | end 22 | end 23 | 24 | def self.encrypt_file(public_key_file_name, input_file_name, output_file_name, **options) 25 | raise ArgumentError.new("Public key file \"#{public_key_file_name}\" does not exist") unless File.exist?(public_key_file_name) 26 | raise ArgumentError.new("Input file \"#{input_file_name}\" does not exist") unless File.exist?(input_file_name) 27 | 28 | recipient = get_recipient(public_key_file_name) 29 | with_temporary_encrypt_keyring(public_key_file_name) do |keyring_file_name| 30 | run_gpg_capture( 31 | *options.map { |k, v| ["--#{k.to_s.gsub('_', '-')}", v ] }.flatten, 32 | '--keyring', keyring_file_name, 33 | '--output', output_file_name, 34 | '--encrypt', 35 | '--armor', 36 | '--recipient', recipient, 37 | '--yes', 38 | '--trust-model', 'always', 39 | '--no-tty', 40 | input_file_name 41 | ) 42 | end 43 | end 44 | 45 | def self.decrypt_file(public_key_file_name, private_key_file_name, input_file_name, output_file_name, passphrase=nil) 46 | raise ArgumentError.new("Public key file \"#{public_key_file_name}\" does not exist") unless File.exist?(public_key_file_name) 47 | raise ArgumentError.new("Private key file \"#{private_key_file_name}\" does not exist") unless File.exist?(private_key_file_name) 48 | raise ArgumentError.new("Input file \"#{input_file_name}\" does not exist") unless File.exist?(input_file_name) 49 | 50 | recipient = get_recipient(private_key_file_name) 51 | with_temporary_decrypt_keyrings(public_key_file_name, private_key_file_name) do |keyring_file_name, secret_keyring_file_name| 52 | args = '--keyring', keyring_file_name, 53 | '--secret-keyring', secret_keyring_file_name, 54 | '--output', output_file_name, 55 | '--decrypt', 56 | '--yes', 57 | '--trust-model', 'always', 58 | '--no-tty', 59 | input_file_name 60 | args.unshift '--passphrase', passphrase unless passphrase.nil? 61 | run_gpg_capture(*args) 62 | end 63 | end 64 | 65 | private 66 | 67 | def self.with_temp_home_dir 68 | Dir.mktmpdir('.rgpg-tmp-', ENV['HOME']) do |home_dir| 69 | yield home_dir 70 | end 71 | end 72 | 73 | def self.build_safe_command_line(home_dir, *args) 74 | fragments = [ 75 | 'gpg', 76 | '--homedir', home_dir, 77 | '--no-default-keyring' 78 | ] + args 79 | fragments.collect { |fragment| Shellwords.escape(fragment) }.join(' ') 80 | end 81 | 82 | def self.run_gpg_no_capture(*args) 83 | with_temp_home_dir do |home_dir| 84 | command_line = build_safe_command_line(home_dir, *args) 85 | result = system(command_line) 86 | raise RuntimeError.new('gpg failed') unless result 87 | end 88 | end 89 | 90 | def self.run_gpg_capture(*args) 91 | with_temp_home_dir do |home_dir| 92 | command_line = build_safe_command_line(home_dir, *args) 93 | 94 | output_file = Tempfile.new('gpg-output') 95 | begin 96 | output_file.close 97 | result = system("#{command_line} > #{Shellwords.escape(output_file.path)} 2>&1") 98 | 99 | output = nil 100 | File.open(output_file.path) do |f| 101 | output = f.read 102 | end 103 | raise RuntimeError.new("gpg failed: #{output}") unless result 104 | 105 | output.lines.collect(&:chomp) 106 | ensure 107 | output_file.unlink 108 | end 109 | end 110 | end 111 | 112 | def self.generate_key_script(public_key_file_name, private_key_file_name, recipient, real_name) 113 | <<-EOS 114 | %echo Generating a standard key 115 | Key-Type: DSA 116 | Key-Length: 1024 117 | Subkey-Type: ELG-E 118 | Subkey-Length: 1024 119 | Name-Real: #{real_name} 120 | Name-Comment: Key automatically generated by rgpg 121 | Name-Email: #{recipient} 122 | Expire-Date: 0 123 | %pubring #{public_key_file_name} 124 | %secring #{private_key_file_name} 125 | # Do a commit here, so that we can later print "done" :-) 126 | %commit 127 | %echo done 128 | EOS 129 | end 130 | 131 | def self.get_recipient(key_file_name) 132 | lines = run_gpg_capture(key_file_name) 133 | result = lines.detect { |line| line =~ /^(pub|sec)\s+\d+(D|R)\/([0-9a-fA-F]{8}).+<(.+)>/ } 134 | raise RuntimeError.new('Invalid output') unless result 135 | key_id = $2 136 | recipient = $3 137 | end 138 | 139 | def self.with_temporary_encrypt_keyring(public_key_file_name) 140 | with_temporary_keyring_file do |keyring_file_name| 141 | run_gpg_capture( 142 | '--keyring', keyring_file_name, 143 | '--import', public_key_file_name 144 | ) 145 | yield keyring_file_name 146 | end 147 | end 148 | 149 | def self.with_temporary_decrypt_keyrings(public_key_file_name, private_key_file_name) 150 | with_temporary_keyring_file do |keyring_file_name| 151 | with_temporary_keyring_file do |secret_keyring_file_name| 152 | run_gpg_capture( 153 | '--keyring', keyring_file_name, 154 | '--secret-keyring', secret_keyring_file_name, 155 | '--import', private_key_file_name 156 | ) 157 | yield keyring_file_name, secret_keyring_file_name 158 | end 159 | end 160 | end 161 | 162 | def self.with_temporary_keyring_file 163 | keyring_file = Tempfile.new('gpg-key-ring') 164 | begin 165 | keyring_file_name = keyring_file.path 166 | keyring_file.close 167 | keyring_file.unlink 168 | yield keyring_file_name 169 | ensure 170 | File.unlink(keyring_file_name) if File.exist?(keyring_file_name) 171 | backup_keyring_file_name = "#{keyring_file_name}~" 172 | File.unlink(backup_keyring_file_name) if File.exist?(backup_keyring_file_name) 173 | end 174 | end 175 | end 176 | end 177 | 178 | --------------------------------------------------------------------------------