Skip to content

Introducing new crypto capabilities in Node.js

Node.js offers powerful new capabilities for generating random UUIDs and random prime numbers.

Much has been happening in the Node.js crypto subsystem lately. We introduced you to Node.js's new Web Cryptography API implementation and the new support for the HKDF key derivation scheme previously, and in this post, we discuss two powerful new capabilities for generating random UUIDs and random prime numbers:

Generating random UUIDs

Universally Unique Identifiers (UUIDs) are surprisingly complex little structures. Most developers look at them and assume they're nothing more than a random sequence of hex-encoded bytes. They don't realise there's actually an IETF RFC detailing the construction and format of multiple variations of UUID — all of which share a common serialisation and structure with significant variations on exactly how the bytes are derived. The irony is that, with the complex definitions and variations that do exist, the random UUID (so-called "version 4 UUIDs") is by far the most popular and widely used.

Historically in Node.js, if you've wanted to generate UUIDs, the go-to module on npm has always been the appropriately named uuid module, a small and useful piece of code that is downloaded over 50 million times per week . I've never seen a production Node.js application that does not have uuid in its dependency tree, and I consider it to be among the most important dependencies in the ecosystem.

So if uuid is ubiquitous, why add uuid generation to Node.js itself?

It's a fair question, with three specific answers:

  1. Philosophically, functionality that is found everywhere really ought to be part of the standard library of the platform.
  2. Implementing random UUID generation directly in Node.js is significantly faster.
  3. The web platform is working to standardise on a crypto.randomUUID() API that is common across environments.

(It's important to note that the uuid module is not going anywhere. It's also worth knowing that uuid module maintainers helped us to review the new API that landed in Node.js core).

Using crypto.randomUUID()

UUIDs aren't simply a sequence of hex-encoded digits. They actually have a structure as defined by RFC 4122:

text
UUID                   = time-low "-" time-mid "-"
                               time-high-and-version "-"
                               clock-seq-and-reserved
                               clock-seq-low "-" node
      time-low               = 4hexOctet
      time-mid               = 2hexOctet
      time-high-and-version  = 2hexOctet
      clock-seq-and-reserved = hexOctet
      clock-seq-low          = hexOctet
      node                   = 6hexOctet
      hexOctet               = hexDigit hexDigit
      hexDigit =
          "0" / "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9" /
          "a" / "b" / "c" / "d" / "e" / "f" /
          "A" / "B" / "C" / "D" / "E" / "F"

Of particular note in this structure are the "version" and "reserved" bits (the time-high-and-version and clock-seq-and-reserved fields in the structure definition above).

The version and reserved fields in the UUID identify the layout and type. As described in RFC 4122, the version is "in the most significant 4 bits of the timestamp (bits 4 through 7 of the time_high_and_version field)", which — if it's not clear — are the four most significant bits of the sixth byte in the UUID. The reserved field identifies the variant which is encoded into the clock-seq-and-reserved field in the eighth byte in the UUID. These are the only fixed values in a random UUID — all other bits in the sequence are randomly generated.

To generate a random UUID in Node.js:

js
import { randomUUID } from 'crypto';
console.log(randomUUID());

This will generate and output such as: 6d38ebd9-5345-4d72-af1d-8ec9ed127235 If you run this multiple times, you will notice there is always a number 4 at the start of the third grouping of numbers (15 characters in). This identifies the version and tells you  this is a randomly generated UUID. Any randomly generated ID that does not have a 4 in this position is not compliant with the standard.

You will also see that the first character in the fourth grouping (20-characters in) will always be a, b, 8 or 9. That is because of the fixed-value reserved field. Any other value in this position means that the UUID is also not compliant with the specification.

All other values in the UUID are generated by Node.js's cryptographic pseudo-random number generator. It is important that we use a cryptographically strong random number generator here to ensure that the UUIDs are random enough to be unpredictable. Many UUID generators end up relying on weaker sources of entropy such as Math.random() in JavaScript. While these low-entropy mechanisms are suitable for many uses, they tend to be highly problematic for the generation of identifiers of any kind.

Because generating reliably unique identifiers can be a performance bottleneck in many applications, doing so as quickly and as efficiently as possible is critical. Improving the performance of random UUID generation was one of the critical requirements of the new Node.js implementation. One way that the Node.js implementation of crypto.randomUUID() achieves a significant performance improvement over the uuid module, and other UUID generation methods is that Node.js will pre-generate and cache enough random data to create up to 128 random UUIDs. Once crypto.randomUUID() has been called enough times to consume that cache, another batch of random data is generated and queued.

To compare the difference in performance between the built-in crypto.randomUUID() and the random UUID generation of the uuid module, consider the following benchmark that looks at the mean, maximum and minimum execution time (in nanoseconds) of the two versions while generating a million UUID's each:

js
const {
  createHistogram,
  performance: {
    timerify
  }
} = require('perf_hooks');

const uuid = require('uuid');
const { randomUUID } = require('crypto');

const h1 = createHistogram();
const h2 = createHistogram();

const t1 = timerify(() => uuid.v4(), { histogram: h1 });
const t2 = timerify(() => randomUUID(), { histogram: h2 });

for (let n = 0; n < 1e6; n++) t1();
for (let n = 0; n < 1e6; n++) t2();

console.log(h1.mean, h1.max, h1.min);
console.log(h2.mean, h2.max, h2.min);

The h1 histogram shows the results for the uuid module. Running on my benchmark server locally, the uuid module has a mean execution time of about 1030 nanoseconds per UUID, with a minimum of 640 and a maximum of 870399. In contrast, crypto.randomUUID() has a mean execution time of only 350 nanoseconds per UUID, with a minimum of 220 and a maximum of 663551. Caching random data is not always desirable, so as an additional option, the Node.js implementation of crypto.randomUUID() allows the cache to be disabled:

js
crypto.randomUUID({ disableEntropyCache: true })

The output generated is identical, but the performance will be reduced because the random data has to be generated on every call.

Generating and checking primes

Prime numbers lie at the heart of many cryptographics operations. Until now, Node.js had mechanisms for using prime numbers in several crypto operations but never had the ability to generate primes.

In Node.js 15.8.0, we introduced the crypto.generatePrime() , crypto.generatePrimeSync() , crypto.checkPrime() , and crypto.checkPrimeSync() APIs to the crypto module. These do precisely what their names suggest. crypto.generatePrime() and crypto.generatePrimeSync() provide the ability to generate a randomly selected prime of a given number of bits .

For example:

js
crypto.generatePrimeSync(32);

Synchronously generates a prime number that is exactly 4-bytes long encoded within an ArrayBuffer. An option can be used to generate the prime as a JavaScript bigint instead:

js
crypto.generatePrimeSync(32, { bigint: true });

Because it can often take a while to calculate larger prime numbers, the crypto.generatePrime() method will perform the calculation in another thread so that the event loop is not blocked.

In some cases, cryptographic operations require primes generated to meet certain specific requirements. A "safe" prime, for instance, is one in which prime - 1 / 2 is also a prime. It is also common to require a prime to be selected such that given the parameters add and rem , prime % add = rem . Whether these conditions are necessary depends entirely on how, where and why the prime is used in the various cryptographic operations. The Node.js API supports these conditions as options:

js
crypto.generatePrime(32, { safe: true, add: 2n, rem : 1n, bigint: true }, (err, prime) => {
  console.log(prime);
})

For the purpose of checking to see if any given number is a prime, we have the crypto.checkPrime() and crypto.checkPrimeSync() methods.

js
crypto.checkPrimeSync(3n)

The semantics of the primality check is that it returns true if the candidate number is a likely prime. There is always a chance that the check may be incorrect. However, the default options for crypto.checkPrime() and crypto.checkPrimeSync() would be expected to yield a false positive rate of at most 2 -64 for randomised input. In other words, the method is accurate enough for the overwhelming majority of cases. The accuracy and the number of checks to be performed can be tuned via the options:

js
crypto.checkPrimeSync(3n, { checks: 10 })

Specifically, the prime checks will return true with an error probability less than 0.25 options.checks .

What's next

These are just a handful of the new capabilities that have been added to Node.js's crypto subsystem in the past year. Other new features include an API for working with X.509 Certificates, support for asynchronously generating and verifying "one-shot" digital signatures and the ability to work with raw ED25519/ED448/X25519/X448 keys. These new capabilities will be explored in future posts.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact