Secure Text Encryption With NodeJS

Perry Mitchell / 2016-06-20 21:50:49
Secure Text Encryption With NodeJS

When it comes to pro­tect­ing elec­tronic in­for­ma­tion, en­cryp­tion is para­mount. Whether it’s send­ing se­cret mes­sages from peer to peer or client to server, or just stor­ing sen­si­tive data lo­cally, en­crypt­ing the in­for­ma­tion be­fore send­ing/​stor­ing it can pro­tect the data so that only the in­tended par­ties can read it.

NodeJS ex­cels at ba­sic en­cryp­tion tech­niques and pro­vides a com­pre­hen­sive built-in API for per­form­ing en­cryp­tion, de­cryp­tion, key de­riva­tion and var­i­ous other en­cod­ing ac­tions. Let’s get into the ba­sics of text en­cryp­tion and how we can achieve a sim­ple yet de­cent im­ple­men­ta­tion in NodeJS.

While Buttercup was in de­vel­op­ment, the text en­cryp­tion func­tion­al­ity was split out into a new li­brary called io­cane. io­cane per­forms highly-se­cure text en­cryp­tion with au­then­ti­ca­tion, and im­ple­ments all of the con­cepts dis­cussed in this ar­ti­cle.

What fa­cil­i­tates suc­cess­ful en­cryp­tion and de­cryp­tion is a key - a key is a piece of in­for­ma­tion that con­trols the en­cryp­tion and de­cryp­tion process (along with other com­po­nents). It is ar­guably the most im­por­tant piece of unique in­for­ma­tion pro­vided dur­ing the en­cryp­tion pro­ce­dure. A key is usu­ally de­rived from a pass­word and a ran­dom set of char­ac­ters (called a salt), and is of a set length (usually a hash like SHA-256 etc.) so that it works well with en­cryp­tion al­go­rithms.

The de­riva­tion process in­volves the pass­word and ran­dom salt be­ing processed in such a way that a hash is gen­er­ated to rep­re­sent the data. The pro­cess­ing of the hash oc­curs in a round, and there are usu­ally many tens or hun­dreds of thou­sands of rounds in a sin­gle de­rived pass­word. The de­riva­tion process is math­e­mat­i­cally com­plex and so takes some time to pro­duce a re­sult - this time de­lay adds to the se­cu­rity of the en­cryp­tion/​de­cryp­tion pro­ce­dure as it would take time for an at­tacker to process the key from a pass­word in the same man­ner.

Deriving a key in Node is quite straight­for­ward:

const crypto = require("crypto");
const ITERATIONS = 1000;
const BYTES = 32;

crypto.pbkdf2("some text", "abc123-salt", ITERATIONS, BYTES, function(err, derivedKey) {
    if (err) {
        // handle the error
    } else {
        var hexEncodedKey = new Buffer(derivedKey).toString('hex');
        // do something with the key
    }
});

Although us­ing Crypto’s pbkdf2 method, I’d rec­om­mend us­ing a third-party wrap­per like pbkdf2 on npm. pbkdf2 pro­vides a neat wrap­per that also sup­ports syn­chro­nous us­age: pbkd2f.pbkdf2Sync('password', 'salt', 1, 32, 'sha512').

With your de­rived key, we can now pro­ceed to the fun part: en­cryp­tion.

Encrypting text

There are many dif­fer­ent flavours of data en­cryp­tion, but we’ll be look­ing at one of the most com­mon: AES CBC. AES Cipher-Block-Chaining is per­haps one of the most well-known and widely used forms of en­cryp­tion as it is both se­cure and widely sup­ported (in terms of plat­forms).

There are ac­tu­ally many dif­fer­ent forms of AES (as well as other ci­phers), though I’d rec­om­mend stick­ing with one that boasts a wide range of sup­port across op­er­at­ing sys­tems. For in­stance, Buttercup was in­tended to be multi-plat­form, so its ci­pher sys­tem would have to be com­pat­i­ble with most com­mon op­er­at­ing sys­tems. AES-GCM was orig­i­nally con­sid­ered, but as iOS did­n’t sup­port it, the im­ple­men­ta­tion was switched to use CBC.

Creating your own ci­pher can be fun and highly ed­u­ca­tional, but is very of­ten strongly dis­cour­aged.. and for good rea­son. Encryption al­go­rithms are highly com­plex and take a long time to reach wide­spread us­age as they’re tested and vet­ted for se­cu­rity and ro­bust­ness. Your own per­sonal ci­pher would most likely not re­ceive the level of scrutiny re­quired to as­sure any­one that it is both safe and rel­a­tively fu­ture-proof.

Let’s dive in to ac­tu­ally en­crypt­ing some­thing with AES-CBC in Node - we just need to con­sider a few pa­ra­me­ters:

  • IV - An ini­tial­i­sa­tion vec­tor
  • Password
  • Salt
  • Key
  • HMAC - Authentication hash
  • Text

We have some text to en­crypt, and we al­ready looked at the pass­word, ran­domly gen­er­ated salt and the key, but what about the IV and HMAC? An ini­tial­i­sa­tion vec­tor en­ables the ci­pher to uniquely gen­er­ate the first block of ci­pher text, which in turn helps to uniquely gen­er­ate the next (and so on):

initialisation vector

IVs are re­quired for the use of CBC mode en­cryp­tion and de­cryp­tion, and it’s best if they’re com­pletely ran­dom. Both IVs and salts can be stored with the re­sult­ing en­crypted con­tent, so they can be col­lected and eas­ily used dur­ing the de­cryp­tion process.

A HMAC is a mes­sage au­then­ti­ca­tion tech­nique, de­signed to help en­sure that the en­crypted con­tent can­not be tam­pered with with­out know­ing about it dur­ing an at­tempted de­cryp­tion. It’s a hash ap­plied to the fi­nal en­crypted con­tent, and usu­ally in­cludes the salt and IV. This hash al­lows the de­crypter to ver­ify that the con­tents of the pack­age were not changed since en­cryp­tion.

Modes like GCM do not need a HMAC, as their au­then­ti­ca­tion mech­a­nism is built into the ci­pher process. NodeJS sup­ports GCM-mode AES en­cryp­tion, though (as men­tioned be­fore) many other plat­forms do not.

Say we have our key:

let key = pbkdf2Sync(password, salt, numberOfRounds, bits, "sha256"),
    hmac;
// HMAC is taken from the key in our case (see iocane)

We can gen­er­ate an IV like so:

let iv = new Buffer(crypto.randomBytes(16)),
    ivHex = iv.toString("hex");

And fi­nally per­form our en­cryp­tion (the HMAC we used came from our key de­riva­tion pro­ce­dure, as with io­cane):

let encryptTool = crypto.createCipheriv("aes-256-cbc", key, iv),
    hmacTool = crypto.createHmac("sha256", hmac),
    saltHex = salt.toString("hex");
// Now encrypt
let encryptedContent = encryptTool.update(text, "utf8", "base64");
encryptedContent += encryptTool.final("base64");
// Generate HMAC
hmacTool.update(encryptedContent);
hmacTool.update(ivHex);
hmacTool.update(saltHex);
let hmacHex = hmacTool.digest("hex");

When stor­ing or trans­fer­ring the con­tent, han­dling the pay­load is made eas­ier by join­ing and split­ting the data:

let package = [encryptedContent, ivHex, saltHex, hmacHex, numberOfRounds].join("$");

The process to per­form se­cure text en­cryp­tion is quite straight­for­ward and eas­ily ab­stracted away into sim­ple helper func­tions. io­cane is a Node pack­age de­signed to pro­vide this sim­ple in­ter­face in a ba­sic & easy-to-use API. It per­forms the en­cryp­tion and pack­ag­ing like I’ve dis­cussed here. io­cane’s de­cryp­tion is just as sim­ple.

Don’t be afraid of en­cryp­tion - there’s a wealth of tools and ex­pe­ri­ence out there to help you se­cure your con­tent.