Decrypting a GPG file in Node JS programmatically
January 21, 2019
I’ve recently worked with GPG - a solution for encrypting/decrypting data for sending over the internet. My task involved receiving an encrypted file which I then decrypt on a Node server. This short post is an overview of how I achieved this.
What is GPG
Firstly, I’ll give a quick explanation of the GPG encryption technology. GPG (or GnuPG) is an implementation of the PGP (Pretty Good Privacy) which is an encryption methodology established in the 1990’s. GPG follows the PGP standard and approach to cryptography, but is easier to use. GPG can be used as a stand-alone software installation, or work as a command line tool with different operating systems.
GPG requires that a user generates a private and public key pair. A file is encrypted (to a .gpg file) using a public key and sent over the internet, and can only be decrypted with the matching private key. When initially creating the private key you must create a password, and this password must be used when decrypting a file with the private key.
A great post I found explains this in detail, along with how to generate your own public and private keys.
Integrating with Node
As I mentioned previously, GPG can be used as an installable program (.dmg file for example) or via the command line. My requirement was that the encrypted file incoming to my Node server needed to be decrypted programmatically using code. In order for the GPG software to work with Node, I found a great library called node-gpg which is a wrapper around the command line GPG software. As such, this means that I had to install three pieces of software:
- GPG - it’s best to install this using Homebrew. Install Homebrew if you haven’t done already, then install GPG in the terminal using
brew install gnupg
. You’ll then be able to use GPG in the terminal. - node-gpg - this is an npm package which can be installed using
npm i gpg
. - pinentry-mac - this was required in order to give the user the option to enter their password for their private key. Without this package, I received an error
gpg: public key decryption failed: Inappropriate ioctl for device
.
The node-gpg
package is a way for Node to interface with the command line instance of GPG. It has a few built in methods like importKey()
, encryptFile()
, and decryptToFile()
. My focus here was on decrypting a file, so the first step is to import the private key which will be used to decrypt the file:
const gpg = require("gpg")
const privateKey = KEY_HERE
gpg.importKey(privateKey, [], (success, err) => {
// run rest of code to decrypt file
})
The privateKey
above will be a huge string, starting with -----BEGIN PGP PRIVATE KEY BLOCK-----
. The key can also be imported from a file, but to keep it simple, I have added it in to a variable for this example. Once the key is imported, it is added to your “keyring”. As you have GPG installed for use on the command line via Homebrew earlier, you can run gpg --list-keys
to show the newly imported key.
You may get an error saying that a armor header is not correct, but this can be ignored.
Now we have the key, we are able to access another method from the node-gpg package to decrypt a file:
const gpg = require("gpg")
const privateKey = KEY_HERE
const fileEncrypted = path.join(__dirname, "/file.txt.gpg")
const fileDecrypted = path.join(__dirname, "/file.txt")
gpg.importKey(privateKey, [], (success, err) => {
gpg.decryptToFile(
{
source: fileEncrypted,
dest: fileDecrypted,
},
(err, success) => {
// success/err
}
)
})
This uses the decryptToFile()
method in order to decrypt the file. Once ran, this will open a popup window asking for the password to authenticate against your private key. This was a major stumbling block in my task as the whole process had to be automated - i.e. I wanted to specify the password on-the-fly without being prompted to enter a password. The app was going to be pushed to a remote server where all this shoulg happen without a user input.
I then decided to use another method from the node-gpg package called callStreaming()
. You may notice this in the README file of the node-gpg package, and it’s basically a way to do direct calls to the brew-installed GPG program, essentially cutting out some of the functionality applied by the node-gpg wrapper.
const gpg = require("gpg")
const privateKey = KEY_HERE
const fileEncrypted = path.join(__dirname, "/file.txt.gpg")
const fileDecrypted = path.join(__dirname, "/file.txt")
gpg.importKey(privateKey, [], (success, err) => {
// args needed in order to skip the password entry - can only
// be used with callStreaming
const args = [
"--pinentry-mode",
"loopback",
"--passphrase",
process.env.PGP_PASS,
]
gpg.callStreaming(fileEncrypted, fileDecrypted, args, (err, success) => {
// success/err
})
})
The gpg.callStreaming()
method takes an array of arguments, which are the arguments one would use when running the core GPG program on the terminal. The above example shows the arguments which are all required. The --pinentry-mode loopback
specifies that we want to skip the manual password entry process, while the --passphrase MY_PASSPHRASE
option specifies the password for the private key. As you can see, I decided to add my password to the server using an environment variable in the form of a .env
file.
The most important thing here is the face that the callStreaming()
method must be used if you want to decrypt using your passphrase programatically. I did also attempt to specify the passphrase from a file rather than adding as a direct argument, which also worked, but only with callStreaming()
- none of the pre-set methods supplied by node-gpg worked.
Running this code on a remote server (such as AWS, DO etc…) will only require that the server has GPG V2 installed, and you’re good to go!
Thanks for reading
That’s it for this short post. Hopefully it will be helpful to someone who wants to perform a similar task. I since tried to encrypt and perform a few other tasks with GPG Node integration, and all worked great.
Senior Engineer at Haven