Skip to content

Commit 4e4deca

Browse files
jasnelldanielleadams
authored andcommitted
crypto: implement randomuuid
Signed-off-by: James M Snell <[email protected]> PR-URL: #36729 Reviewed-By: Сковорода Никита Андреевич <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Michaël Zasso <[email protected]> Reviewed-By: Filip Skokan <[email protected]> Reviewed-By: Ben Coe <[email protected]>
1 parent a956fb3 commit 4e4deca

File tree

6 files changed

+237
-2
lines changed

6 files changed

+237
-2
lines changed

benchmark/crypto/randomUUID.js

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const { randomUUID } = require('crypto');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [1e7],
8+
disableEntropyCache: [0, 1],
9+
});
10+
11+
function main({ n, disableEntropyCache }) {
12+
disableEntropyCache = !!disableEntropyCache;
13+
bench.start();
14+
for (let i = 0; i < n; ++i)
15+
randomUUID({ disableEntropyCache });
16+
bench.end(n);
17+
}

doc/api/crypto.md

+16
Original file line numberDiff line numberDiff line change
@@ -3160,6 +3160,21 @@ const n = crypto.randomInt(1, 7);
31603160
console.log(`The dice rolled: ${n}`);
31613161
```
31623162

3163+
### `crypto.randomUUID([options])`
3164+
<!-- YAML
3165+
added: REPLACEME
3166+
-->
3167+
3168+
* `options` {Object}
3169+
* `disableEntropyCache` {boolean} By default, to improve performance,
3170+
Node.js will pre-emptively generate and persistently cache enough
3171+
random data to generate up to 128 random UUIDs. To generate a UUID
3172+
without using the cache, set `disableEntropyCache` to `true`.
3173+
**Defaults**: `false`.
3174+
* Returns: {string}
3175+
3176+
Generates a random [RFC 4122][] Version 4 UUID.
3177+
31633178
### `crypto.scrypt(password, salt, keylen[, options], callback)`
31643179
<!-- YAML
31653180
added: v10.5.0
@@ -3929,6 +3944,7 @@ See the [list of SSL OP Flags][] for details.
39293944
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
39303945
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
39313946
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
3947+
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
39323948
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
39333949
[Web Crypto API documentation]: webcrypto.md
39343950
[`Buffer`]: buffer.md

lib/crypto.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ const {
5353
randomBytes,
5454
randomFill,
5555
randomFillSync,
56-
randomInt
56+
randomInt,
57+
randomUUID,
5758
} = require('internal/crypto/random');
5859
const {
5960
pbkdf2,
@@ -199,6 +200,7 @@ module.exports = {
199200
randomFill,
200201
randomFillSync,
201202
randomInt,
203+
randomUUID,
202204
scrypt,
203205
scryptSync,
204206
sign: signOneShot,

lib/internal/crypto/random.js

+109-1
Original file line numberDiff line numberDiff line change
@@ -12,24 +12,28 @@ const {
1212
RandomBytesJob,
1313
kCryptoJobAsync,
1414
kCryptoJobSync,
15+
secureBuffer,
1516
} = internalBinding('crypto');
1617

1718
const {
1819
lazyDOMException,
1920
} = require('internal/crypto/util');
2021

21-
const { kMaxLength } = require('buffer');
22+
const { Buffer, kMaxLength } = require('buffer');
2223

2324
const {
2425
codes: {
2526
ERR_INVALID_ARG_TYPE,
2627
ERR_OUT_OF_RANGE,
28+
ERR_OPERATION_FAILED,
2729
}
2830
} = require('internal/errors');
2931

3032
const {
3133
validateNumber,
34+
validateBoolean,
3235
validateCallback,
36+
validateObject,
3337
} = require('internal/validators');
3438

3539
const {
@@ -281,10 +285,114 @@ function getRandomValues(data) {
281285
return data;
282286
}
283287

288+
// Implements an RFC 4122 version 4 random UUID.
289+
// To improve performance, random data is generated in batches
290+
// large enough to cover kBatchSize UUID's at a time. The uuidData
291+
// and uuid buffers are reused. Each call to randomUUID() consumes
292+
// 16 bytes from the buffer.
293+
294+
const kHexDigits = [
295+
48, 49, 50, 51, 52, 53, 54, 55,
296+
56, 57, 97, 98, 99, 100, 101, 102
297+
];
298+
299+
const kBatchSize = 128;
300+
let uuidData;
301+
let uuidNotBuffered;
302+
let uuid;
303+
let uuidBatch = 0;
304+
305+
function getBufferedUUID() {
306+
if (uuidData === undefined) {
307+
uuidData = secureBuffer(16 * kBatchSize);
308+
if (uuidData === undefined)
309+
throw new ERR_OPERATION_FAILED('Out of memory');
310+
}
311+
312+
if (uuidBatch === 0) randomFillSync(uuidData);
313+
uuidBatch = (uuidBatch + 1) % kBatchSize;
314+
return uuidData.slice(uuidBatch * 16, (uuidBatch * 16) + 16);
315+
}
316+
317+
function randomUUID(options) {
318+
if (options !== undefined)
319+
validateObject(options, 'options');
320+
const {
321+
disableEntropyCache = false,
322+
} = { ...options };
323+
324+
validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
325+
326+
if (uuid === undefined) {
327+
uuid = Buffer.alloc(36, '-');
328+
uuid[14] = 52; // '4', identifies the UUID version
329+
}
330+
331+
let uuidBuf;
332+
if (!disableEntropyCache) {
333+
uuidBuf = getBufferedUUID();
334+
} else {
335+
uuidBuf = uuidNotBuffered;
336+
if (uuidBuf === undefined)
337+
uuidBuf = uuidNotBuffered = secureBuffer(16);
338+
if (uuidBuf === undefined)
339+
throw new ERR_OPERATION_FAILED('Out of memory');
340+
randomFillSync(uuidBuf);
341+
}
342+
343+
// Variant byte: 10xxxxxx (variant 1)
344+
uuidBuf[8] = (uuidBuf[8] & 0x3f) | 0x80;
345+
346+
// This function is structured the way it is for performance.
347+
// The uuid buffer stores the serialization of the random
348+
// bytes from uuidData.
349+
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
350+
let n = 0;
351+
uuid[0] = kHexDigits[uuidBuf[n] >> 4];
352+
uuid[1] = kHexDigits[uuidBuf[n++] & 0xf];
353+
uuid[2] = kHexDigits[uuidBuf[n] >> 4];
354+
uuid[3] = kHexDigits[uuidBuf[n++] & 0xf];
355+
uuid[4] = kHexDigits[uuidBuf[n] >> 4];
356+
uuid[5] = kHexDigits[uuidBuf[n++] & 0xf];
357+
uuid[6] = kHexDigits[uuidBuf[n] >> 4];
358+
uuid[7] = kHexDigits[uuidBuf[n++] & 0xf];
359+
// -
360+
uuid[9] = kHexDigits[uuidBuf[n] >> 4];
361+
uuid[10] = kHexDigits[uuidBuf[n++] & 0xf];
362+
uuid[11] = kHexDigits[uuidBuf[n] >> 4];
363+
uuid[12] = kHexDigits[uuidBuf[n++] & 0xf];
364+
// -
365+
// 4, uuid[14] is set already...
366+
uuid[15] = kHexDigits[uuidBuf[n++] & 0xf];
367+
uuid[16] = kHexDigits[uuidBuf[n] >> 4];
368+
uuid[17] = kHexDigits[uuidBuf[n++] & 0xf];
369+
// -
370+
uuid[19] = kHexDigits[uuidBuf[n] >> 4];
371+
uuid[20] = kHexDigits[uuidBuf[n++] & 0xf];
372+
uuid[21] = kHexDigits[uuidBuf[n] >> 4];
373+
uuid[22] = kHexDigits[uuidBuf[n++] & 0xf];
374+
// -
375+
uuid[24] = kHexDigits[uuidBuf[n] >> 4];
376+
uuid[25] = kHexDigits[uuidBuf[n++] & 0xf];
377+
uuid[26] = kHexDigits[uuidBuf[n] >> 4];
378+
uuid[27] = kHexDigits[uuidBuf[n++] & 0xf];
379+
uuid[28] = kHexDigits[uuidBuf[n] >> 4];
380+
uuid[29] = kHexDigits[uuidBuf[n++] & 0xf];
381+
uuid[30] = kHexDigits[uuidBuf[n] >> 4];
382+
uuid[31] = kHexDigits[uuidBuf[n++] & 0xf];
383+
uuid[32] = kHexDigits[uuidBuf[n] >> 4];
384+
uuid[33] = kHexDigits[uuidBuf[n++] & 0xf];
385+
uuid[34] = kHexDigits[uuidBuf[n] >> 4];
386+
uuid[35] = kHexDigits[uuidBuf[n] & 0xf];
387+
388+
return uuid.latin1Slice(0, 36);
389+
}
390+
284391
module.exports = {
285392
randomBytes,
286393
randomFill,
287394
randomFillSync,
288395
randomInt,
289396
getRandomValues,
397+
randomUUID,
290398
};

src/crypto/crypto_util.cc

+34
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ using v8::NewStringType;
3131
using v8::Nothing;
3232
using v8::Object;
3333
using v8::String;
34+
using v8::Uint32;
35+
using v8::Uint8Array;
3436
using v8::Value;
3537

3638
namespace crypto {
@@ -587,6 +589,36 @@ CryptoJobMode GetCryptoJobMode(v8::Local<v8::Value> args) {
587589
return static_cast<CryptoJobMode>(mode);
588590
}
589591

592+
namespace {
593+
// SecureBuffer uses openssl to allocate a Uint8Array using
594+
// OPENSSL_secure_malloc. Because we do not yet actually
595+
// make use of secure heap, this has the same semantics as
596+
// using OPENSSL_malloc. However, if the secure heap is
597+
// initialized, SecureBuffer will automatically use it.
598+
void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
599+
CHECK(args[0]->IsUint32());
600+
Environment* env = Environment::GetCurrent(args);
601+
uint32_t len = args[0].As<Uint32>()->Value();
602+
char* data = static_cast<char*>(OPENSSL_secure_malloc(len));
603+
if (data == nullptr) {
604+
// There's no memory available for the allocation.
605+
// Return nothing.
606+
return;
607+
}
608+
memset(data, 0, len);
609+
std::shared_ptr<BackingStore> store =
610+
ArrayBuffer::NewBackingStore(
611+
data,
612+
len,
613+
[](void* data, size_t len, void* deleter_data) {
614+
OPENSSL_secure_clear_free(data, len);
615+
},
616+
data);
617+
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
618+
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
619+
}
620+
} // namespace
621+
590622
namespace Util {
591623
void Initialize(Environment* env, Local<Object> target) {
592624
#ifndef OPENSSL_NO_ENGINE
@@ -600,6 +632,8 @@ void Initialize(Environment* env, Local<Object> target) {
600632

601633
NODE_DEFINE_CONSTANT(target, kCryptoJobAsync);
602634
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
635+
636+
env->SetMethod(target, "secureBuffer", SecureBuffer);
603637
}
604638
} // namespace Util
605639

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
const assert = require('assert');
9+
const {
10+
randomUUID,
11+
} = require('crypto');
12+
13+
const last = new Set([
14+
'00000000-0000-0000-0000-000000000000'
15+
]);
16+
17+
function testMatch(uuid) {
18+
assert.match(
19+
uuid,
20+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
21+
}
22+
23+
// Generate a number of UUID's to make sure we're
24+
// not just generating the same value over and over
25+
// and to make sure the batching changes the random
26+
// bytes.
27+
for (let n = 0; n < 130; n++) {
28+
const uuid = randomUUID();
29+
assert(!last.has(uuid));
30+
last.add(uuid);
31+
assert.strictEqual(typeof uuid, 'string');
32+
assert.strictEqual(uuid.length, 36);
33+
testMatch(uuid);
34+
35+
// Check that version 4 identifier was populated.
36+
assert.strictEqual(
37+
Buffer.from(uuid.substr(14, 2), 'hex')[0] & 0x40, 0x40);
38+
39+
// Check that clock_seq_hi_and_reserved was populated with reserved bits.
40+
assert.strictEqual(
41+
Buffer.from(uuid.substr(19, 2), 'hex')[0] & 0b1100_0000, 0b1000_0000);
42+
}
43+
44+
// Test non-buffered UUID's
45+
{
46+
testMatch(randomUUID({ disableEntropyCache: true }));
47+
testMatch(randomUUID({ disableEntropyCache: true }));
48+
testMatch(randomUUID({ disableEntropyCache: true }));
49+
testMatch(randomUUID({ disableEntropyCache: true }));
50+
51+
assert.throws(() => randomUUID(1), {
52+
code: 'ERR_INVALID_ARG_TYPE'
53+
});
54+
55+
assert.throws(() => randomUUID({ disableEntropyCache: '' }), {
56+
code: 'ERR_INVALID_ARG_TYPE'
57+
});
58+
}

0 commit comments

Comments
 (0)