ecma-nacl

JavaScript (ECMAScript) version of NaCl Cryptographic library

npm install ecma-nacl
20 downloads in the last week
26 downloads in the last month

ecma-nacl: Pure JavaScript (ECMAScript) version of NaCl cryptographic library.

NaCl is a great crypto library that is not placing a burden of crypto-math choices onto developers, providing only solid high-level functionality (box - for public-key, and secret_box - for secret key authenticated encryption), in a let's stop blaming users of cryptographic library (e.g. end product developers, or us) manner. Take a look at details of NaCl's design "The security impact of a new cryptographic library".

ecma-nacl is a re-write of most important NaCl's functionality, which is ready for production, box and secret_box. Signing code still has XXX comments, indicating that the warning in signing in NaCl should be taken seriously.

Rewrite is based on the copy of NaCl, included in this repository. Tests are written to correspond those in C code, to make sure that output of this library is the same as that of C's version. Besides this, we added comparison runs between ecma-nacl, and js-nacl, which is an Emscripten-compilation of C library. These comparison runs can be done in both node and browsers.

As of version 0.5, js-nacl is properly compiled into asm.js code, recognized by Firefox. Proper asm.js does run two times faster than pure js. So, we have a tradeoff here: with js-nacl one gets speed, which realistically matters on client side when encrypting 100s MBs of data, and with ecma-nacl one gets smaller library size and better auditability. Asm.js might still be a moving target, while ecma-nacl is exploiting only those JS features that has already been widely implemented. At the same time, js-nacl keeps C-ish api, while ecma-nacl is providing JS-ish api, adding convenient packaging features. In fact, we are confident, that things like XSP file format, introduced in ecma-nacl api, shall spread beyond JS implementations of NaCl.

NPM Package

This library is registered on npmjs.org. To install it:

npm install ecma-nacl

Browser Package

make-browserified.js will let you make a browserified module. So, make sure that you have browserify module to run the script. You may also modify it to suite your particular needs.

API for secret-key authenticated encryption

Add module into code as

var nacl = require('ecma-nacl');

Secret-key authenticated encryption is provided by secret_box, which implements XSalsa20+Poly1305, and nothing else.

When encrypting, or packing, NaCl does following things. First, it encrypts plain text bytes using XSalas20 algorithm. Secondly, it creates 16 bytes of authentication Poly1305 code, and places these infront of the cipher. Thus, regular byte layout is 16 bytes of Poly1305 code, followed by cipher with actual message, having exactly the same length as plain text message.

Decrypting, or opening goes through these steps in reverse. First, Poly1305 code is read and is compared with code, generated by reading cipher. When these do not match, it means either that key+nonce pair is incorrect, or that cipher with message has been damaged/changed. Our code will throw an exception in such a case. Secondly, when verification is successful, XSalsa20 will do decryption, producing message bytes.

// all incoming and outgoing things are Uint8Array's;
// to encrypt, or pack plain text bytes into cipher bytes, use
var cipher_bytes = nacl.secret_box.pack(plain_bytes, nonce, key);

// decryption, or opening is done by
var result_bytes = nacl.secret_box.open(cipher_bytes, nonce, key);

Above pack method will produce an Uint8Array with cipher, offset by 16 zero bytes in the underlying buffer. Deciphered bytes, on the other hand, are offset by 32 zero bytes. This should always be kept in mind, when transferring raw buffers to/from web-workers. In all other places, this padding is never noticed, thanks to typed array api.

Key is 32 bytes long. Nonce is 24 bytes. Nonce means number-used-once, i.e. it should be unique for every segment encrypted by the same key.

Sometimes, when storing things, it is convenient to pack cipher together with nonce (WN) into the same array.

+-------+ +------+ +---------------+
| nonce | | poly | |  data cipher  |
+-------+ +------+ +---------------+
| <----       WN format      ----> |

For this, secret_box has formatWN object, which is used analogously:

// encrypting, and placing nonce as first 24 bytes infront NaCl's byte output layout
var cipher_bytes = nacl.secret_box.formatWN.pack(plain_bytes, nonce, key);

// decryption, or opening is done by
var result_bytes = nacl.secret_box.formatWN.open(cipher_bytes, key);

// extraction of nonce from cipher can be done as follows
var extracted_nonce = nacl.secret_box.formatWN.copyNonceFrom(cipher_bytes);

Cipher array here has no offset in the buffer, but decrypted array does have the same 32 zero bytes offset, as mentioned above.

It is important to always use different nonce, when encrypting something new with the same key. A function is provided, to advance nonce. The 24 bytes are taken as three 32-bit integers, and are advanced by 1 (oddly) or by 2 (evenly). So, when encrypting many segments of a huge file, advance nonce oddly every time. When key is shared, and is used for communication between two parties, one party's initial nonce may be oddly advanced initial nonce, received from the second party, and all other respective nonces are advanced evenly on both sides of communication. This way, unique nonces are used for every message send.

// nonce changed in place oddly
nacl.advanceNonceOddly(nonce);

// nonce changed in place evenly
nacl.advanceNonceEvenly(nonce);

It is common, that certain code needs to be given encryption/decryption functionality, but according to principle of least authority such code does not necessarily need to know secret key, with which encryption is done. So, there is an encryptor for opening and packing, with inbuilt even advance of the nonce, on every new cipher that is generated. It is made to produce and read ciphers with-nonce format.

var encryptor = nacl.secret_box.formatWN.makeEncryptor(key, nextNonce);

// packing bytes is done with
var cipher_bytes = encryptor.pack(plain_bytes);

// opening is done with
var result_bytes = encryptor.open(cipher_bytes);

// when encryptor is no longer needed, key should be properly wiped from memory
encryptor.destroy();

API for public-key authenticated encryption

Public-key authenticated encryption is provided by box, which implements Curve25519+XSalsa20+Poly1305, and nothing else. Given pairs of secret-public keys, corresponding shared, in Diffie–Hellman sense, key is calculated (Curve25519) and is used for data encryption with secret_box (XSalsa20+Poly1305).

Given any random secret key, we can generate corresponding public key:

var public_key = nacl.box.generate_pubkey(secret_key);

Secret key may come from browser's crypto.getRandomValues(array), or be derived from passphrase with js-scrypt, which is an emscripten-compiled original C library.

There are two ways to use box. The first way is to always do two things, calculation of DH-shared key and subsequent packing/opening, in one step.

// Alice encrypts message for Bob
var cipher_bytes = nacl.box.pack(msg_bytes, nonce, bob_pkey, alice_skey);

// Bob opens the message
var msg_bytes = nacl.box.open(cipher_bytes, nonce, alice_pkey, bob_skey);

The second way is to calculate DH-shared key once and use it for packing/opening multiple messages, with box.stream.pack and box.stream.open, which are just nicknames of described above secret_box.pack and secret_box.open.

// Alice calculates DH-shared key
var dhshared_key = nacl.box.calc_dhshared_key(bob_pkey, alice_skey);
// Alice encrypts message for Bob
var cipher_bytes = nacl.box.stream.pack(msg_bytes, nonce, dhshared_key);

// Bob calculates DH-shared key
var dhshared_key = nacl.box.calc_dhshared_key(alice_pkey, bob_skey);
// Bob opens the message
var msg_bytes = nacl.box.stream.open(cipher_bytes, nonce, dhshared_key);

Or, we may use box encryptors that do first step of DH-shared key calculation only at creation.

Alice's side:

// generate nonce, browser example
var nonce = new Uint8Array(24);
crypto.getRandomValues(nonce);

// make encryptor to produce with-nonce format
var encryptor = nacl.box.formatWN.makeEncryptor(bob_pkey, alice_skey, nonce);

// pack messages to Bob
var cipher_to_send = encryptor.pack(msg_bytes);

// open mesages from Bob
var msg_from_bob = encryptor.open(received_cipher);

// when encryptor is no longer needed, key should be properly wiped from memory
encryptor.destroy();

Bob's side:

// get nonce from Alice's first message, advance it oddly, and
// use for encryptor, as encryptors on both sides advance nonces evenly
var nonce = nacl.box.formatWN.copyNonceFrom(cipher1_from_alice);
nacl.advanceNonceOddly(nonce);

// make encryptor to produce with-nonce format
var encryptor = nacl.box.formatWN.makeEncryptor(alice_pkey, bob_skey, nonce);

// pack messages to Alice
var cipher_to_send = encryptor.pack(msg_bytes);

// open mesages from Alice
var msg_from_alice = encryptor.open(received_cipher);

// when encryptor is no longer needed, key should be properly wiped from memory
encryptor.destroy();

Random number generation

NaCl does not do it. The randombytes in the original code is a unix shim with the following rational, given in the comment, quote: "it's really stupid that there isn't a syscall for this".

So, you should obtain cryptographically strong random bytes yourself. In node, there is crypto. There is crypto in browser. IE6? IE6 must die! Stop supporting insecure crap! Respect your users, and tell them truth, that they need modern secure browser(s).

Signing

It is still not settled into production code in NaCl. Period. When it is ready, we will be able to serve it.

DNSCurve is questioning whether common places of signing be better served with public-key encryption.

XSP file format

Each NaCl's cipher must be read completely, before any plain text output. Such requirement makes reading big files awkward. Thus, the simplest solution is to pack NaCl's binary ciphers into self-contained small segments, each encrypted with a different nonce. Such segment-based format is also useful in a streaming situation, when one end starts to send a file, without knowing when EOF comes.

We call this format XSP, to stand for XSalsa+Poly, to indicate that file layout is specifically tailored for storing NaCl's secret box's ciphers.

Each segment contains a header. The first segment contains a file header, followed by a common segment header.

File header layout:

+-----+  +-------------------+ +--------------+
| xsp |  | file key envelope | | max seg size |
+-----+  +-------------------+ +--------------+

First three bytes is a constant ASCII encoded string 'xsp'. This takes up first 3 bytes.

"File key envelope" is an encrypted, and packed with-nonce (WN format), key, used to encrypt every segment of a given file. Length of this envelope calculated as follows: key is 32 bytes, add 16 Poly bytes, and 24 nonce. This gives 72 bytes for file key envelope.

"Max seg size" is 4 bytes with a maximum, or common segment length, written in little endian way. Total length of any segment in this file should never exceed given value.

Thus, file header, has a predictable length of 79 bytes.

Segment layout:

| <-- segment header  --> |
+-------------------------+ +---------------+
| seg size | nonce | poly | |  data cipher  |
+-------------------------+ +---------------+
           | <----      WN format     ----> |

Segment starts with 4 bytes, containing total length of this segment, written in little endian way. Next 40 bytes contain nonce and poly bytes. These first 44 bytes we call a segment header.

Notice that nonce and poly in the header are layed out so, that together with the following data cipher, they constitute WN pack format.

First segment layout:

+-------------+ +----------------+ +---------------+
| file header | | segment header | |  data cipher  |
+-------------+ +----------------+ +---------------+

Data cipher are the crypto stream bytes, into which data was xor-ed. Therefore, data cipher's length is exactly the same as encoded data's length.

API for packing/opening XSP segments

There is a sub-module, with XSP-related functionality:

var xsp = nacl.fileXSP;

Each xsp object has two encrypted parts that use different keys. One part is the content, encrypted with file key. Another part is an encrypted file key package, which is encrypted with some master key. Thus, to instantiate encryptor, we need a file key, and a function that will encrypt it to a master key. Notice, that we can provide master key itself, but, a principle of least priviledge dictates that we should provide only required master key's encrypting capability, and nothing else.

// we should get or generate a file key
var fileKey = foo();
// given master key encryptor, we can take its pack function
var fileKeyPackFunc = masterKeyEnc.pack;
var fileKeyOpenFunc = masterKeyEnc.open;

There are two situations, in which xsp encryptor can be initialized. First situation is when there is no existing xsp file:

var enc = xsp.makeNewFileEncryptor(maxSegSize, fileKey, fileKeyPackFunc);

Second situation is when file exists:

var enc = xsp.makeExistingFileEncryptor(firstSegHeader, fileKeyOpenFunc);

Encryptor can pack Uint8Array data into segments:

var segments = []
, dataOffset = 0;

// packing first segment uses special method
var encRes = enc.packFirstSegment(data, nonce);

// result object has created segment, and a number of packed data bytes
segments.push( encRes.seg );
dataOffset += encRes.dataLen;

// nonce must be changed for use in a different segment
nacl.advanceNonceOddly(nonce);

while(dataOffset < data.length) {

    // packing other segments
    encRes = enc.packSegment(data, nonce);

    segments.push( encRes.seg );
    dataOffset += encRes.dataLen;
    nacl.advanceNonceOddly(nonce);
}

Encryptor can open segments (notice how incoming arrays must be alligned with segments' starting point), given complete xsp file as Uint8Array:

var fileOffset = 0
, dataParts = []
, decRes;

while(fileOffset < xspFile.length) {

    decRes = enc.openSegment( xspFile.subarray(fileOffset) );

    // result object has opened data, and segment's size, read from file
    dataParts.push( decRes.data );
    fileOffset += decRes.segLen
}

Encryptors should be properly disposed after use:

enc.destroy();

License

This code is provided here under Mozilla Public License Version 2.0.

NaCl C library is public domain code by Daniel J. Bernstein and others crypto gods and semi-gods. We thank thy wisdom of giving us developer-friendly library.

npm loves you