CommsCentral

My Technology Adventures

OpenPGP.js how to encrypt files

Posted at: 2018-02-03 @ 07:51:39

Quick post today on using the openpgp.js library to create an encrypted file that is PGP/GPG compatible.
This was another one of those things that took me longer to implement than I expected!

I write most of my javascript using typescript these days, so that's what I'm showing in my examples. If you want raw JS, see my GitHub or tsc (type script compiler) the example code.

First, let us look at the encryption.
I'm going to supply the key in a format called ASCII armour .
In this format a public key will start with -----BEGIN PGP PUBLIC KEY BLOCK-----

When using this format, pulling the key in requires using the readArmored method:

const publicKey: string = '-----BEGIN PGP PUBLIC KEY BLOCK-----.......';

openpgp.initWorker({}); // initialise openpgpjs
const openpgpPublicKey = openpgp.key.readArmored(publicKey);


Ok now we need to deal with a file. Unfortunately openpgp.js only supports files in Uint8Array, but fs.readFile by default will return a node buffer. Thankfully this isn't that hard!

const file = fs.readFileSync('/tmp/file-to-be-encryped');
const fileForOpenpgpjs = new Uint8Array(file);


Next, we build the request object for openpgp.js. Note, how we actually reference the .keys item attached to the openpgpPublicKey object response of readArmored key

const options = {
data: fileForOpenpgpjs,
publicKeys: openpgpPublicKey.keys,
armor: false
};

The armor: false - setting tells openpgp js not to ascii armor the file output. Hence, We will get a binary object NOT a string. openpgp does support a full file in ASCII armour, I'm unsure if that would be a good idea for a file of any significant size.

Setup complete, now we perform the actual encryption!

const encryptionResponse = await openpgp.encrypt(options); // note the await here - this is async operation


This performs the encryption, now we need to get the file object out of the system, that requires calling message.packets.write()

const encryptedFile = encryptionResponse.message.packets.write();


Right now we have a constant, encryptedFile that is of type Uint8Array. Lucky for us fs.writeFile supports Uint8Array as an input! So we simply write it back to disk!

fs.writeFileSync('/tmp/file-encryped', encryptedFile);


Phew ok, encryption done.

Guess we'd better be able to decrypt, starts much the same.
As private keys can have many sub keys we have to pick one. Hence .keys[0] on readArmored

There is another new thing, .decrypt. Private pgp keys are encrypted, so we have to perform a decrypt using our private key password.
Lastly, we do the cast from buffer to Uint8Array again on our file read

const privateKey: string = '-----BEGIN PGP PRIVATE KEY BLOCK-----.......';
openpgp.initWorker({});

const openpgpPrivateKeyObject = openpgp.key.readArmored(privateKey).keys[0];
openpgpPrivateKeyObject.decrypt('PRIVATE KEY PASSWORD');

const file = fs.readFileSync('/tmp/file-encryped');
const fileForOpenpgpjs = new Uint8Array(file);



Next is our openpgp request options. Little different to encrypt which took me ages to figure out!

const options = {
privateKey: openpgpPrivateKeyObject,
message: openpgp.message.read(fileForOpenpgpjs),
format: 'binary'
};

Fairly obvious now you see it, right?.. The format binary is important telling openpgp.js that we are dealing with a binary, not a ASCII armoured message.


Now we call the actual decrypt. Again this is an async function, so I'm using await here and the raw binary file will be in the .data attribute contains the Uint8Array that is our file.
This we can write directly back to disk as fs supports Uint8Array as an input.

const decryptionResponse = await openpgp.decrypt(options);
const decryptedFile = decryptionResponse.data
fs.writeFileSync('/tmp/file-encryped-then-decrypted', decryptedFile);


Last but not least, it is probably a good idea to include the following at the start of your file, these operations prevent openpgp.js printing version and comment information into any files.

openpgp.config.show_version = false;
openpgp.config.show_comment = false;


Once you have seen it and had it explained it is not that hard. I ended up working this out by reading the openpgp.js unit test code... Not ideal really, so I hope this helps someone out there!

One word of warning. As we're reading files into buffers here and not handling them as streams (I don't think openpgp.js supports streams) there is a risk we could run the system out of memory on a file larger than available memory. I'd already check input file size vs available memory if you can.

All the source is available at https://github.com/adcreare/nodejs-demos/tree/master/openpgp.js





© 2015 CommsCentral