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:
- Ruby installed on your system
- The Open4 gem installed (
gem install open4
) - GPG installed on your system
Understanding the Approach
We’ll be creating a Ruby class that:
- Uses GPG via system commands
- Leverages the Open4 gem for better process management
- 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:
- It gives us access to the process’s standard input, output, and error streams
- It provides better process management and status handling
- It allows for more controlled execution of external commands
GPG Command Structure
Our class builds GPG commands with various options:
--homedir
: Specifies the GPG home directory--batch
: Runs GPG in non-interactive mode--yes
: Automatically confirms any prompts--trust-model always
: Skips trust verification--recipient
: Specifies the encryption recipient (email or key ID)--output
: Defines the output file path
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:
-
Passphrase Management: Be careful with how you handle passphrases in your application. Avoid storing them in plaintext or passing them through insecure channels.
-
Key Management: Properly secure GPG key files and consider implementing key rotation policies.
-
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.
-
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:
- Key Generation: Add methods to generate new GPG keys
- Signing: Implement file signing capabilities
- Verification: Add methods to verify signed files
- 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:
- You need to integrate GPG encryption into a Ruby application
- You prefer using the established GPG binary rather than pure-Ruby cryptography implementations
- You need access to the full range of GPG features
Remember that while this implementation provides a solid foundation, you should adapt it to your specific security requirements and use cases.