Skip to content

Commit

Permalink
crypto: implement randomuuid
Browse files Browse the repository at this point in the history
Signed-off-by: James M Snell <jasnell@gmail.com>

PR-URL: #36729
Reviewed-By: Сковорода Никита Андреевич <chalkerx@gmail.com>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Ben Coe <bencoe@gmail.com>
  • Loading branch information
jasnell authored and danielleadams committed Jan 12, 2021
1 parent a956fb3 commit 4e4deca
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 2 deletions.
17 changes: 17 additions & 0 deletions benchmark/crypto/randomUUID.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use strict';

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

const bench = common.createBenchmark(main, {
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,
scrypt,
scryptSync,
sign: signOneShot,
Expand Down
110 changes: 109 additions & 1 deletion lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
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;
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);
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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(
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'
});
}

0 comments on commit 4e4deca

Please sign in to comment.