Decrypting with GPG 2.2 & Ruby
Problem
You are trying to decrypt a file using GPG and you get the following error:
Inappropriate ioctl for device
Potential Solution
It depends on what you are trying to accomplish and where you are trying to accomplish it. It might be that all you need to do is update your GPG package to the most recent version for the code to work properly. If you got an error trying to set up a new project or use a new gem I would suspect the version of GPG on your machine is incompatible with the code you are trying to run.
If you are in control of the code being used to decrypt a file then the following might be helpful.
Importing a private key:
Decrypting a file:
What the hell does any of that mean?
That is a great question!
Let’s start with what we are trying to accomplish. If you made it this far I am guessing you are trying to decrypt (or encrypt) a PGP file with Ruby. The only problem is that, as far as I am aware, the only tools built for processing PGP encryption are system tools. In other words, the machine can do it but Ruby cannot.
Luckily, Ruby gives us the tools we need to access the system it is running in. The most common usecases that I have seen are backticks.
Backticks
I literally use the example above ALL THE TIME because I am frequently copying a column of data from a spreadsheet and parsing it into code to then iterate over.
Try this one out in your terminal. Copy some text, open irb
, and type:
Hint: a backtick is the symbol to the left of the number one on a qwerty keyboard.
Press return and voila, the text you copied should be what is returned. pbpaste
is a system program that you are accessing by telling Ruby to execute it with backticks.
Open3
Backticks are good for short, simple, easy to read snippets. I like Open3 because it helps break up more complicated system commands into readable chunks as well as providing additional tools to help out. Apart from readability, I think its biggest advantage comes from error handling.
My method of choice in the Open3
class is capture2e
. Documentation can be found here. The arguments passed to capture2e
are comma separated strings. The return value of capture2e
is an array with two elements: the output of the system program and the status of that program.
Destructuring
output, status = Open3.capture2e
An easy way of capturing and assigning all elements in an array at once is with destructuring. Other than being neat to know this is not something that you will often encounter in Ruby code.
Error Handling
For our case, the return values of capture2e
are not valuable at all for the processing of the file. The program GPG handles that entirely. What they are extremely useful for is if something goes wrong. When writing code with no user interface, I like having verbose errors to catch when code does not execute as I expect it to.
As we can see in the example above, we can call .success?
on status
to get a true
or false
response. Then we signal to Ruby to raise an exception with the message “could not import private key” followed by the output of the system program. An example error might look something like:
RuntimeError (could not import private key:
Inappropriate ioctl for device)
Arguments
"gpg",
"--pinentry-mode", "loopback",
"--passphrase-fd", "0",
"--output", output_file_path,
"--decrypt", encrypted_file_path,
stdin_data: encryption_passphrase
The first string we pass to capture2e
is the string gpg
. gpg
is the name of the system program we are using to decrypt the file. Documentation for gpg
and all its options can be found here.
The next strings we pass to capture2e
are the arguments that we need to pass to gpg
to get it to do what we want.
Long story short, the gpg
program defaults to a user interface that Ruby cannot interact with. The options we are passing, pinentry-mode
and passphrase-fd
specifically, are modifying how the program accepts passwords being sent to it.
When we are importing a private key, we will need access to the file path of the private key file on the machine. If we know where it lives we can supply it with the option import
.
When we are decrypting a file, we will need access to the file path of the encrypted file. We can supply the file path with the option decrypt
. We will also need to identify a file path for gpg
to write the decrypted file which we can supply with the output
option.
The last argument is stdin_data
. This is where we can input the passphrase required by gpg
. We already told gpg
that we would be supplying the passphrase using the system’s standard input with the pinentry-mode
and passphrase-fd
options. By supplying a hash with the passphrase, capture2e
runs the commands then sends the passphrase to gpg
.
To learn more about how GPG works I highly recommend the documentation. While it is not light reading and very likely to induce an early death from boredom, it was invaluable when I was trying to figure out how to build the correct arguments.
Putting it all together, we can import a private key to gpg
, send the corresponding passphrase programmatically, and supply an encrypted file to gpg
for it to decrypt and write to the disk.