Ruby GPG File Encryption/Decryption Using System Commands with Open4

30 Mar 2025 - Gagan Shrestha

In today’s security-conscious world, encrypting sensitive data is essential for protecting information from unauthorized access. GPG (GNU Privacy Guard) provides a robust solution for encrypting and decrypting files. In this post, we’ll explore how to implement GPG encryption and decryption in Ruby using the Open4 gem to manage system commands.

Prerequisites

Before diving into the code, make sure you have:

  1. Ruby installed on your system
  2. The Open4 gem installed (gem install open4)
  3. GPG installed on your system

Understanding the Approach

We’ll be creating a Ruby class that:

  1. Uses GPG via system commands
  2. Leverages the Open4 gem for better process management
  3. Provides a clean, reusable interface for encryption and decryption operations

The RubyGPG Class Implementation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
require 'open4'
require 'fileutils'

class RubyGPG
  attr_reader :last_error, :last_status

  def initialize(options = {})
    @gpg_binary = options[:gpg_binary] || 'gpg'
    @gpg_home = options[:gpg_home] || ENV['GNUPGHOME'] || File.join(ENV['HOME'], '.gnupg')
    @last_error = nil
    @last_status = nil
  end

  def encrypt(file_path, recipient, output_path = nil)
    output_path ||= "#{file_path}.gpg"

    # Build the command
    cmd = [
      @gpg_binary,
      "--homedir", @gpg_home,
      "--batch",
      "--yes",
      "--trust-model", "always",
      "--encrypt",
      "--recipient", recipient,
      "--output", output_path,
      file_path
    ]

    execute_command(cmd)
    output_path if @last_status == 0
  end

  def decrypt(file_path, passphrase = nil, output_path = nil)
    # Default output path removes .gpg extension if present
    output_path ||= file_path.end_with?('.gpg') ? file_path[0...-4] : "#{file_path}.decrypted"

    # Build the command
    cmd = [
      @gpg_binary,
      "--homedir", @gpg_home,
      "--batch",
      "--yes",
      "--decrypt",
      "--output", output_path
    ]

    # Add passphrase handling if provided
    env = {}
    if passphrase
      cmd.unshift("--passphrase-fd", "0")
      env["PASSPHRASE"] = passphrase
    end

    cmd << file_path

    execute_command(cmd, env, passphrase)
    output_path if @last_status == 0
  end

  def import_key(key_file)
    cmd = [
      @gpg_binary,
      "--homedir", @gpg_home,
      "--batch",
      "--import", key_file
    ]

    execute_command(cmd)
    @last_status == 0
  end

  def list_keys
    cmd = [
      @gpg_binary,
      "--homedir", @gpg_home,
      "--list-keys"
    ]

    stdout = ""
    execute_command(cmd) { |out| stdout = out }
    stdout if @last_status == 0
  end

  private

  def execute_command(cmd, env = {}, input = nil)
    stdout, stderr = "", ""

    begin
      status = Open4.popen4(env, cmd.join(' ')) do |pid, stdin, stdout_io, stderr_io|
        # Write passphrase to stdin if provided
        if input
          stdin.puts(input)
          stdin.close
        end

        # Read output streams
        stdout = stdout_io.read
        stderr = stderr_io.read
      end

      @last_status = status.exitstatus
      @last_error = stderr unless stderr.empty?
      yield(stdout) if block_given?

      return true
    rescue => e
      @last_error = e.message
      @last_status = -1
      return false
    end
  end
end

How to Use the RubyGPG Class

Let’s walk through some common use cases for our new RubyGPG class:

1. Initializing the Class

1
2
3
4
5
6
7
8
# Basic initialization with defaults
gpg = RubyGPG.new

# Custom initialization
gpg = RubyGPG.new(
  gpg_binary: '/usr/local/bin/gpg',
  gpg_home: '/custom/path/to/.gnupg'
)

2. Encrypting a File

1
2
3
4
5
6
7
8
9
10
11
12
# Encrypt file.txt for recipient [email protected]
# Output will be file.txt.gpg
gpg = RubyGPG.new
encrypted_file = gpg.encrypt('file.txt', '[email protected]')

# Encrypt with custom output path
encrypted_file = gpg.encrypt('file.txt', '[email protected]', 'encrypted_output.gpg')

# Check for errors
unless encrypted_file
  puts "Encryption failed: #{gpg.last_error}"
end

3. Decrypting a File

1
2
3
4
5
6
7
8
9
10
11
# Basic decryption (output will be file.txt)
gpg = RubyGPG.new
decrypted_file = gpg.decrypt('file.txt.gpg')

# Decrypt with passphrase and custom output
decrypted_file = gpg.decrypt('file.txt.gpg', 'my_secret_passphrase', 'decrypted_output.txt')

# Check for errors
unless decrypted_file
  puts "Decryption failed: #{gpg.last_error}"
end

4. Managing Keys

1
2
3
4
5
6
7
8
9
10
11
# Import a public key
gpg = RubyGPG.new
if gpg.import_key('public_key.asc')
  puts "Key imported successfully"
else
  puts "Key import failed: #{gpg.last_error}"
end

# List available keys
keys = gpg.list_keys
puts keys if keys

How It Works

The Open4 Gem

The Open4 gem provides a more robust way to manage system processes compared to Ruby’s built-in system or backtick methods:

  1. It gives us access to the process’s standard input, output, and error streams
  2. It provides better process management and status handling
  3. It allows for more controlled execution of external commands

GPG Command Structure

Our class builds GPG commands with various options:

Error Handling

The class captures both exit status codes and error messages, making it easy to determine if an operation succeeded and, if not, why it failed.

Security Considerations

When implementing cryptographic solutions, keep these security considerations in mind:

  1. Passphrase Management: Be careful with how you handle passphrases in your application. Avoid storing them in plaintext or passing them through insecure channels.

  2. Key Management: Properly secure GPG key files and consider implementing key rotation policies.

  3. Process Security: Remember that system commands can introduce security vulnerabilities if not properly sanitized. Our class handles command construction safely, but be cautious when modifying it.

  4. Temporary Files: GPG sometimes creates temporary files. Ensure your application runs in an environment with secure temporary directories.

Extending the Class

You might want to extend this class with additional functionality:

  1. Key Generation: Add methods to generate new GPG keys
  2. Signing: Implement file signing capabilities
  3. Verification: Add methods to verify signed files
  4. Multiple Recipients: Enhance the encrypt method to support multiple recipients

Conclusion

The RubyGPG class provides a simple but powerful interface for GPG encryption and decryption operations in Ruby applications. By leveraging the Open4 gem, we gain better control over the GPG process execution while maintaining a clean, object-oriented interface.

This approach is particularly useful when:

Remember that while this implementation provides a solid foundation, you should adapt it to your specific security requirements and use cases.

Resources