This post is about simple-crypt, a Javascript wrapper library for encrypting and signing data.

Here's what I did and didn't want to do with simple-crypt:

  • Don't re-implement any cryptographic algorithms.
  • Don't get into key exchange protocols.
  • Do write extensive unit tests, ideally including some well-known test vectors.
  • Do make a consistent API for encrypting and signing data using different types of keys:
    • Symmetric keys
    • Asymmetric keys
    • Password-derived keys
  • Do hardcode which encryption and signature algorithms to use:
    • HMAC-SHA-256 for symmetric signing.
    • RSA-SHA-256 with RSASSA-PSS encoding for asymmetric signing.
    • AES-128-CBC for symmetric key encryption.
    • RSA, RSAES-OAEP encoding and AES-128-CBC for asymmetric encryption.
  • Do add a checksum before encrypting data.
  • Do JSON encode data before encrypting or signing it.
  • Do make it easy to disable processing and pass plaintext through instead.
  • Do make it easy to add metadata so the recipient knows which key to use.

Basic usage

Here's what I came up with. Everything revolves around the Crypt class. Say you have some key (symmetric, public, private or password). Then you'd pass it to Crypt.make to make a Crypt object like this:

Crypt.make(key, function (err, crypt)
{
    // if err exists then handle it
    // otherwise crypt is a Crypt object
});

You can then use crypt to sign, encrypt, verify or decrypt data. For example, using a symmetric key:

key = crypto.randomBytes(Crypt.get_key_size());

Crypt.make(key, function (err, crypt)
{
    crypt.sign({ foo: 90 }, function (err, signed)
    {
        // send 'signed' somewhere else
    });
});

// somewhere else (how to get exchange key securely is left to the application)...
Crypt.make(key, function (err, crypt)
{
    crypt.verify(signed, function (err, verified)
    {
        assert.deepEqual(verified, { foo: 90 });
    });
});

Keys

Symmetric keys

Symmetric keys should be fixed length. On Node.js you might make one like this:

var key = crypto.randomBytes(Crypt.get_key_size());

Asymmetric keys

You can pass PEM-encoded RSA public and private keys to Crypt.make:

var priv_pem = '-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA4qiw...Se2gIJ/QJ3YJVQI=\n-----END RSA PRIVATE KEY-----';
var pub_pem = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w...BoeoyBrpTuc4egSCpj\nsQIDAQAB\n-----END PUBLIC KEY-----';

Use the public key to encrypt and the private key to decrypt:

Crypt.make(pub_pem, function (err, crypt)
{
    crypt.encrypt({ bar: 'hi' }, function (err, encrypted)
    {
        // send 'encrypted' somewhere else
    });
});

// somewhere else...
Crypt.make(priv_pem, function (err, crypt)
{
    crypt.decrypt(encrypted, function (err, decrypted)
    {
        assert.deepEqual(decrypted, { bar: 'hi' });
    });
});

(the private key can also sign and the public key verify).

Passwords

You'll need to specify the number of iterations (for PBKDF2) along with the password, for example:

var key = { password: 'P@ssW0rd!', iterations: 10000 };

Producing plaintext

I wanted to be able to turn off signing or encryption easily and pass the data through untouched. Here's how:

Crypt.make(key, function (err, crypt)
{
    var whether_to_encrypt = false;
    crypt.maybe_encrypt(whether_to_encrypt, 123.456, function (err, encrypted)
    {
        // send 'encrypted' somewhere else...
    });
});

// somewhere else has to call maybe_decrypt...
Crypt.make(key, function (err, crypt)
{
    crypt.maybe_decrypt(encrypted, function (err, decrypted)
    {
        assert.equal(decrypted, 123.456);
    });
});

There are maybe_sign and maybe_verify too.

Adding metadata

Unencrypted metadata can be added alongside the encrypted payload. Typically the recipient would use it to determine which key to use to decrypt the actual data.

Instead of passing the key to Crypt.make, you pass a function to maybe_encrypt which supplies the key and metadata:

var keys = { super_secret_sensor_29: 'some random key!' };
var data = { device_id: 'super_secret_sensor_29', value: 42 };

Crypt.make().maybe_encrypt(data, function (err, encrypted)
{
    // send 'encrypted' somewhere else
}, function (device_id, cb) // must supply key and metadata to 'cb'
{
    cb(null, keys[device_id], device_id);
}, data.device_id /* any metadata you want to pass into the function */);

// somewhere else...

Crypt.make().maybe_decrypt(encrypted, function (err, decrypted)
{
    assert.deepEqual(decrypted, data);
}, function (cb, device_id) // receives metadata, must supply key to 'cb'
{
    cb(null, keys[device_id]);
});

What simple-crypt does not do

simple-crypt doesn't implement the signing and encryption algorithms itself.

simple-crypt doesn't say anything about how to exchange keys. If you want Perfect Forward Secrecy then you might consider using something like Diffie-Hellman to exchange symmetric keys. You might also need some kind of public key infrastructure to support your asymmetric keys.

Finally, simple-crypt doesn't get into key derivation. Key derivation algorithms are useful if you intend to use the same key for multiple purposes. simple-crypt expects any key derivation to be done beforehand — i.e. it expects to be used with the derived key.

The reason for this is that it's impossible to cater for the wide range of ancillary data which might be fed into a key derivation algorithm. For examples of key derivation functions, consider:

Where to get it

You can find the simple-crypt source, API documentation and unit tests here.

Please let me know if you have a problem or spot something wrong!



blog comments powered by Disqus