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 NaCl in TypeScript, which is ECMAScript with compile-time types. Library implements NaCl's box and secret box. Signing code comes from SUPERCOP version 2014-11-24.
The following have been added to ecma-nacl beyond NaCl. Firstly, it is a re-write of scrypt key derivation function. Scrypt is a highly valuable thing for services that allow users to have passwords, while doing proper work with crypto keys, derived from those passwords. Secondly, it is an XSP file format for objects encrypted with NaCl.
Re-writes are based on C sources, 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. Scrypt's tests a taken from an RFC draft. Besides this, we added comparison runs between ecma-nacl, and js-nacl with js-scrypt, which is an Emscripten-compilation of C respective libraries. These comparison runs can be done in both node and browsers.
This library is registered on npmjs.org. To install it, do:
npm install ecma-nacl
Package comes with already compiled library code in dist/ folder. For testing, bring up a build environment (see below), and run necessary gulp tasks.
Once you get package, or this repo, do in the folder
which will install dev-dependencies.
Building is done with gulp, so it has to also be installed globally, as this is how gulp functions. From there on do in the folder
and have fun with it.
Add module into code as
var nacl = require'ecma-nacl';
Secret-key authenticated encryption is provided by secret_box, which implements XSalsa20+Poly1305.
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. 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, usevar cipher_bytes = naclsecret_boxpackplain_bytes nonce key;// decryption, or opening is done byvar result_bytes = naclsecret_boxopencipher_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 layoutvar cipher_bytes = naclsecret_boxformatWNpackplain_bytes nonce key;// decryption, or opening is done byvar result_bytes = naclsecret_boxformatWNopencipher_bytes key;// extraction of nonce from cipher can be done as followsvar extracted_nonce = naclsecret_boxformatWNcopyNonceFromcipher_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. Object nonce contains functions to advance nonce, to calculate consequent nonces, etc. The 24 bytes of a nnce are taken as three 32-bit integers, and are advanced by 1 (oddly), by 2 (evenly), or by n. So, when encrypting many segments of a huge file, one can 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 oddlynaclnonceadvanceOddlynonce;// nonce changed in place evenlynaclnonceadvanceEvenlynonce;// nonce changed in place by deltanaclnonceadvancenonce delta;// calculate related noncevar relatedNonce = naclnoncecalculateNoncenonce delta arrayFactory;// find delta between nonces (null result is for unrelated nonces)var delta = naclnoncecalculateDeltan1 n2;
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, one may make an encryptor and a decryptor. These are made to produce and read ciphers with-nonce format.
// delta is optional, defaults to onevar encryptor = naclsecret_boxformatWNmakeEncryptorkey nextNonce delta;// packing bytes is done withvar cipher_bytes = encryptorpackplain_bytes;// when encryptor is no longer needed, key should be properly wiped from memoryencryptordestroy;var decryptor = naclsecret_boxformatWNmakeDecryptorkey;// opening is done withvar result_bytes = decryptoropencipher_bytes;// when encryptor is no longer needed, key should be properly wiped from memorydecryptordestroy;
Public-key authenticated encryption is provided by box, which implements Curve25519+XSalsa20+Poly1305. 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 = naclboxgenerate_pubkeysecret_key;
Secret key may come from browser's crypto.getRandomValues(array), or be derived from a passphrase with scrypt.
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 Bobvar cipher_bytes = naclboxpackmsg_bytes nonce bob_pkey alice_skey;// Bob opens the messagevar msg_bytes = naclboxopencipher_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 keyvar dhshared_key = naclboxcalc_dhshared_keybob_pkey alice_skey;// Alice encrypts message for Bobvar cipher_bytes = naclboxstreampackmsg_bytes nonce dhshared_key;// Bob calculates DH-shared keyvar dhshared_key = naclboxcalc_dhshared_keyalice_pkey bob_skey;// Bob opens the messagevar msg_bytes = naclboxstreamopencipher_bytes nonce dhshared_key;
Or, we may use box encryptors that do first step of DH-shared key calculation only at creation.
// generate nonce, browser examplevar nonce = 24;cryptogetRandomValuesnonce;// make encryptor to produce with-nonce format (default delta is two)var encryptor = naclboxformatWNmakeEncryptorbob_pkey alice_skey nonce;// pack messages to Bobvar cipher_to_send = encryptorpackmsg_bytes;// open mesages from Bobvar decryptor = naclboxformatWNmakeDecryptorbob_pkey alice_skey;var msg_from_bob = decryptoropenreceived_cipher;// when encryptor is no longer needed, key should be properly wiped from memoryencryptordestroy;decryptordestroy;
// get nonce from Alice's first message, advance it oddly, and// use for encryptor, as encryptors on both sides advance nonces evenlyvar nonce = naclboxformatWNcopyNonceFromcipher1_from_alice;naclnonceadvanceOddlynonce;// make encryptor to produce with-nonce format (default delta is two)var encryptor = naclboxformatWNmakeEncryptoralice_pkey bob_skey nonce;// pack messages to Alicevar cipher_to_send = encryptorpackmsg_bytes;// open mesages from Alicevar decryptor = naclboxformatWNmakeDecryptoralice_pkey bob_skey;var msg_from_alice = encryptoropenreceived_cipher;// when encryptor is no longer needed, key should be properly wiped from memoryencryptordestroy;decryptordestroy;
signing object contains all related functionality.
// signing key pair can be generated from some seed array, which can// either be random itself, or be generated from a passwordvar pair = naclsigninggenerate_keypairseed;// make signature bytes, for msgvar msgSig = naclsigningsignaturemsg pairskey;// verify signaturevar sigIsOK = naclsigningverifymsgSig msg pairpkey;
There are functions like NaCl's sign and sign_open methods, which place signature and message into one array, and expect the same for opening (verification). In a context of JWK, abovementioned functions seem to be more flexible and useful than C's API.
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).
Scrypt derives a key from users password.
Algorithm is memory-hard, which means it uses lots and lots of memory.
There are three parameters that go into derivation:
Amount of memory used is roughly
128 * N * r == r * 2^(7+logN) bytes.
r = 8 ,
logN is 10, it is a MB range of memory,
logN is 20, it is a GB range of memory in use.
p says how many times should the whole operation occur.
So, when running out of memory (js is not giving enough memory for
logN = 20
), one may up
It goes without saying, that such operations take time, and this implementation has a callback for progress reporting.
// given pass (secret), salt and other less-secret parameters// key of length keyLen is generated as follows:var logN = 17;var r = 8;var p = 2;var key = naclscryptpass salt logN r p keyLenconsole.log'derivation progress: ' + pDone + '%';;
Colin Percival's paper about scrypt makes for a lovely weekend-long reading.
Each NaCl's cipher must be read completely, before any plain text output. Such requirement makes reading big files awkward. Therefore, we need a file format, which satisfies the following requirements:
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. XSP file has segments, which are NaCl's packs of Poly and XSalsa cipher, and a header. Header is stored at the end of the file, when segments and header are stored in a single file object. Sometimes, XSP segments and a header can be stored in separate files.
Complete XSP file looks as following (starting with UTF-8 'xsp'):
+-----+ +---------------+ +----------+ +--------+ | xsp | | header offset | | segments | | header | +-----+ +---------------+ +----------+ +--------+
When file parts are stored separately, header file starts with UTF-8 'hxsp', and segments' file starts with UTF-8 'sxsp'.
Header can be of two types, distinguished by header's size.
(72+65)-byte header is used for a file with unknown length (endless file), and looks as follows:
|<- 72 bytes ->| |<- 65 bytes ->| +---------------+ +--------------------------------+ | file key | | seg size | first segment nonce | +---------------+ +--------------------------------+ |<- WN format ->| |<- WN format ->|
Let's define a segment chain to be a series of segments, encrypted with related nonces. Chain requires three parameters for a comfortable use:
Endless file has only one segment chain, and both length of the last segment, and a number of segments cannot be known.
(72+46+n*30)-byte header is used for a file with known length (finite file). n is a number of segment chains in the file. Header layout:
|<- 72 bytes ->| |<- 46+n*30 bytes ->| +---------------+ +-----------------------------------------------+ | file key | | total segs length | | seg size | | seg chains | +---------------+ +-----------------------------------------------+ |<- WN format ->| |<- WN format ->|
Segment chain bytes look as following:
|<- 4 bytes ->| |<- 2 bytes ->| |<- 24 bytes ->| +----------------+ +---------------+ +---------------------+ | number of segs | | last seg size | | first segment nonce | +----------------+ +---------------+ +---------------------+
There is a sub-module, with XSP-related functionality:
var xsp = naclfileXSP;
Start work with any file by creating an object that holds file key, and, therefore, can generate correct readera and writers.
// for a new file, the following generates new file key, contained in the holdervar fkeyHolder = xspmakeNewFileKeyHoldermkeyEncr getRandom;// existing file has its key packed in its header, which should be usedvar fkeyHolder = xspmakeFileKeyHoldermkeyDecr header;
Packing segments and header:
// new file writer needs segment size and a function to get random bytesvar writer = fkeyHoldernewSegWritersegSizein256bs getRandom;// header is produced by the following callvar header = writerpackHeader;// segments are packed withvar sInfo = writerpackSegcontent segInd;// where sInfo.dataLen is a number of content bytes packed,// and sInfo.seg is an array with segment bytes.// initial endless file can be set to be finite, this changes header informationwritersetContentLengthcontentLen;// writer of existing file should read existing headervar writer = xspsegmentsmakeWriterheader getRandom;// writer should be destroyed, when no longer neededwriterdestroy;
Currently, at version 2.2.0, efficient splicing functionality is not implemented, but it shall exist, as file format allows for it. We also plan to add some fool-proof restrictions into implementation to disallow packing the same segment twice. For now, users are advised to be careful, and to pack each segment only once.
Reader is used for reading:
var reader = xspsegmentsmakeReaderheader masterKeyDecr;var dInfo = readeropenSegseg segInd;// where dInfo.segLen is a number of segment files read,// where dInfo.last is true for the last segment of this file,// and dInfo.data is an array with data, read from the segment.// reader should be destroyed, when no longer neededreaderdestroy;
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.
Scrypt C library is created by Colin Percival. We thank thee for bringing us strong new ideas in key derivation tasks.