Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: implement randomuuid #36729

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions benchmark/crypto/randomUUID.js
@@ -0,0 +1,17 @@
'use strict';

const common = require('../common.js');
const { randomUUID } = require('crypto');

const bench = common.createBenchmark(main, {
jasnell marked this conversation as resolved.
Show resolved Hide resolved
n: [1e7],
disableEntropyCache: [0, 1],
});

function main({ n, disableEntropyCache }) {
disableEntropyCache = !!disableEntropyCache;
bench.start();
for (let i = 0; i < n; ++i)
randomUUID({ disableEntropyCache });
bench.end(n);
}
16 changes: 16 additions & 0 deletions doc/api/crypto.md
Expand Up @@ -3160,6 +3160,21 @@ const n = crypto.randomInt(1, 7);
console.log(`The dice rolled: ${n}`);
```

### `crypto.randomUUID([options])`
<!-- YAML
added: REPLACEME
-->

* `options` {Object}
* `disableEntropyCache` {boolean} By default, to improve performance,
Node.js will pre-emptively generate and persistently cache enough
random data to generate up to 128 random UUIDs. To generate a UUID
without using the cache, set `disableEntropyCache` to `true`.
**Defaults**: `false`.
* Returns: {string}

Generates a random [RFC 4122][] Version 4 UUID.

### `crypto.scrypt(password, salt, keylen[, options], callback)`
<!-- YAML
added: v10.5.0
Expand Down Expand Up @@ -3929,6 +3944,7 @@ See the [list of SSL OP Flags][] for details.
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
[Web Crypto API documentation]: webcrypto.md
[`Buffer`]: buffer.md
Expand Down
4 changes: 3 additions & 1 deletion lib/crypto.js
Expand Up @@ -53,7 +53,8 @@ const {
randomBytes,
randomFill,
randomFillSync,
randomInt
randomInt,
randomUUID,
} = require('internal/crypto/random');
const {
pbkdf2,
Expand Down Expand Up @@ -199,6 +200,7 @@ module.exports = {
randomFill,
randomFillSync,
randomInt,
randomUUID,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding naming: The function name randomUUID() was initially proposed and discussed in tc39/proposal-uuid#3 (comment)

We were also discussing renaming the api of the uuid npm module (uuidjs/uuid#540 (comment)) but came to the conclusion that we don't want to do that while the standardization efforts are still ongoing.

After all I still think the term randomUUID has a lot of benefits since it clearly describes what this is about whereas the whole concept of "version" within the UUID RFC has proven to be highly confusing, see my detailed analysis.

It also avoids naming conflicts with the current uuid npm module so this should remove confusion when upgrading from the npm module to the native implementation.

So all in all I'm 👍 for naming it like that in Node.js

scrypt,
scryptSync,
sign: signOneShot,
Expand Down
110 changes: 109 additions & 1 deletion lib/internal/crypto/random.js
Expand Up @@ -12,24 +12,28 @@ const {
RandomBytesJob,
kCryptoJobAsync,
kCryptoJobSync,
secureBuffer,
} = internalBinding('crypto');

const {
lazyDOMException,
} = require('internal/crypto/util');

const { kMaxLength } = require('buffer');
const { Buffer, kMaxLength } = require('buffer');

const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
ERR_OPERATION_FAILED,
}
} = require('internal/errors');

const {
validateNumber,
validateBoolean,
validateCallback,
validateObject,
} = require('internal/validators');

const {
Expand Down Expand Up @@ -281,10 +285,114 @@ function getRandomValues(data) {
return data;
}

// Implements an RFC 4122 version 4 random UUID.
// To improve performance, random data is generated in batches
// large enough to cover kBatchSize UUID's at a time. The uuidData
// and uuid buffers are reused. Each call to randomUUID() consumes
// 16 bytes from the buffer.

const kHexDigits = [
48, 49, 50, 51, 52, 53, 54, 55,
56, 57, 97, 98, 99, 100, 101, 102
];

const kBatchSize = 128;
jasnell marked this conversation as resolved.
Show resolved Hide resolved
let uuidData;
let uuidNotBuffered;
let uuid;
let uuidBatch = 0;

function getBufferedUUID() {
if (uuidData === undefined) {
uuidData = secureBuffer(16 * kBatchSize);
if (uuidData === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
}

if (uuidBatch === 0) randomFillSync(uuidData);
puzpuzpuz marked this conversation as resolved.
Show resolved Hide resolved
uuidBatch = (uuidBatch + 1) % kBatchSize;
return uuidData.slice(uuidBatch * 16, (uuidBatch * 16) + 16);
}

function randomUUID(options) {
if (options !== undefined)
validateObject(options, 'options');
const {
disableEntropyCache = false,
} = { ...options };

validateBoolean(disableEntropyCache, 'options.disableEntropyCache');

if (uuid === undefined) {
uuid = Buffer.alloc(36, '-');
uuid[14] = 52; // '4', identifies the UUID version
}

let uuidBuf;
if (!disableEntropyCache) {
uuidBuf = getBufferedUUID();
} else {
uuidBuf = uuidNotBuffered;
if (uuidBuf === undefined)
uuidBuf = uuidNotBuffered = secureBuffer(16);
if (uuidBuf === undefined)
throw new ERR_OPERATION_FAILED('Out of memory');
randomFillSync(uuidBuf);
}

// Variant byte: 10xxxxxx (variant 1)
uuidBuf[8] = (uuidBuf[8] & 0x3f) | 0x80;

// This function is structured the way it is for performance.
// The uuid buffer stores the serialization of the random
// bytes from uuidData.
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
let n = 0;
uuid[0] = kHexDigits[uuidBuf[n] >> 4];
uuid[1] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[2] = kHexDigits[uuidBuf[n] >> 4];
uuid[3] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[4] = kHexDigits[uuidBuf[n] >> 4];
uuid[5] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[6] = kHexDigits[uuidBuf[n] >> 4];
uuid[7] = kHexDigits[uuidBuf[n++] & 0xf];
// -
uuid[9] = kHexDigits[uuidBuf[n] >> 4];
uuid[10] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[11] = kHexDigits[uuidBuf[n] >> 4];
uuid[12] = kHexDigits[uuidBuf[n++] & 0xf];
// -
// 4, uuid[14] is set already...
uuid[15] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[16] = kHexDigits[uuidBuf[n] >> 4];
uuid[17] = kHexDigits[uuidBuf[n++] & 0xf];
// -
uuid[19] = kHexDigits[uuidBuf[n] >> 4];
uuid[20] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[21] = kHexDigits[uuidBuf[n] >> 4];
uuid[22] = kHexDigits[uuidBuf[n++] & 0xf];
// -
uuid[24] = kHexDigits[uuidBuf[n] >> 4];
uuid[25] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[26] = kHexDigits[uuidBuf[n] >> 4];
uuid[27] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[28] = kHexDigits[uuidBuf[n] >> 4];
uuid[29] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[30] = kHexDigits[uuidBuf[n] >> 4];
uuid[31] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[32] = kHexDigits[uuidBuf[n] >> 4];
uuid[33] = kHexDigits[uuidBuf[n++] & 0xf];
uuid[34] = kHexDigits[uuidBuf[n] >> 4];
uuid[35] = kHexDigits[uuidBuf[n] & 0xf];

return uuid.latin1Slice(0, 36);
}

module.exports = {
randomBytes,
randomFill,
randomFillSync,
randomInt,
getRandomValues,
randomUUID,
};
34 changes: 34 additions & 0 deletions src/crypto/crypto_util.cc
Expand Up @@ -31,6 +31,8 @@ using v8::NewStringType;
using v8::Nothing;
using v8::Object;
using v8::String;
using v8::Uint32;
using v8::Uint8Array;
using v8::Value;

namespace crypto {
Expand Down Expand Up @@ -587,6 +589,36 @@ CryptoJobMode GetCryptoJobMode(v8::Local<v8::Value> args) {
return static_cast<CryptoJobMode>(mode);
}

namespace {
// SecureBuffer uses openssl to allocate a Uint8Array using
// OPENSSL_secure_malloc. Because we do not yet actually
// make use of secure heap, this has the same semantics as
// using OPENSSL_malloc. However, if the secure heap is
// initialized, SecureBuffer will automatically use it.
void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
CHECK(args[0]->IsUint32());
Environment* env = Environment::GetCurrent(args);
uint32_t len = args[0].As<Uint32>()->Value();
char* data = static_cast<char*>(OPENSSL_secure_malloc(len));
if (data == nullptr) {
// There's no memory available for the allocation.
// Return nothing.
return;
}
memset(data, 0, len);
std::shared_ptr<BackingStore> store =
ArrayBuffer::NewBackingStore(
data,
len,
[](void* data, size_t len, void* deleter_data) {
OPENSSL_secure_clear_free(data, len);
},
data);
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
}
} // namespace

namespace Util {
void Initialize(Environment* env, Local<Object> target) {
#ifndef OPENSSL_NO_ENGINE
Expand All @@ -600,6 +632,8 @@ void Initialize(Environment* env, Local<Object> target) {

NODE_DEFINE_CONSTANT(target, kCryptoJobAsync);
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);

env->SetMethod(target, "secureBuffer", SecureBuffer);
}
} // namespace Util

Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-crypto-randomuuid.js
@@ -0,0 +1,58 @@
'use strict';

const common = require('../common');

if (!common.hasCrypto)
common.skip('missing crypto');

const assert = require('assert');
const {
randomUUID,
} = require('crypto');

const last = new Set([
'00000000-0000-0000-0000-000000000000'
]);

function testMatch(uuid) {
assert.match(
uuid,
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
}

// Generate a number of UUID's to make sure we're
// not just generating the same value over and over
// and to make sure the batching changes the random
// bytes.
for (let n = 0; n < 130; n++) {
const uuid = randomUUID();
assert(!last.has(uuid));
last.add(uuid);
assert.strictEqual(typeof uuid, 'string');
assert.strictEqual(uuid.length, 36);
testMatch(uuid);

// Check that version 4 identifier was populated.
assert.strictEqual(
jasnell marked this conversation as resolved.
Show resolved Hide resolved
Buffer.from(uuid.substr(14, 2), 'hex')[0] & 0x40, 0x40);

// Check that clock_seq_hi_and_reserved was populated with reserved bits.
assert.strictEqual(
Buffer.from(uuid.substr(19, 2), 'hex')[0] & 0b1100_0000, 0b1000_0000);
}

// Test non-buffered UUID's
{
testMatch(randomUUID({ disableEntropyCache: true }));
testMatch(randomUUID({ disableEntropyCache: true }));
testMatch(randomUUID({ disableEntropyCache: true }));
testMatch(randomUUID({ disableEntropyCache: true }));

assert.throws(() => randomUUID(1), {
code: 'ERR_INVALID_ARG_TYPE'
});

assert.throws(() => randomUUID({ disableEntropyCache: '' }), {
code: 'ERR_INVALID_ARG_TYPE'
});
}