Skip to content

Commit 1b3860c

Browse files
committed
crypto: implement randomuuid
Signed-off-by: James M Snell <[email protected]> Original-PR-URL: #36729
1 parent 5b5bed9 commit 1b3860c

File tree

8 files changed

+259
-9
lines changed

8 files changed

+259
-9
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

+15
Original file line numberDiff line numberDiff line change
@@ -2841,6 +2841,20 @@ const n = crypto.randomInt(1, 7);
28412841
console.log(`The dice rolled: ${n}`);
28422842
```
28432843

2844+
### `crypto.randomUUID([options])`
2845+
<!-- YAML
2846+
added: REPLACEME
2847+
-->
2848+
2849+
* `options` {Object}
2850+
* `disableEntropyCache` {boolean} By default, to improve performance,
2851+
Node.js generates and caches enough random data to generate up to
2852+
128 random UUIDs. To generate a UUID without using the cache, set
2853+
`disableEntropyCache` to `true`. **Defaults**: `false`.
2854+
* Returns: {string}
2855+
2856+
Generates a random [RFC 4122][] Version 4 UUID.
2857+
28442858
### `crypto.scrypt(password, salt, keylen[, options], callback)`
28452859
<!-- YAML
28462860
added: v10.5.0
@@ -3582,6 +3596,7 @@ See the [list of SSL OP Flags][] for details.
35823596
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
35833597
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
35843598
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt
3599+
[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt
35853600
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
35863601
[`Buffer`]: buffer.md
35873602
[`EVP_BytesToKey`]: https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html

doc/api/errors.md

+6
Original file line numberDiff line numberDiff line change
@@ -1704,6 +1704,12 @@ compiled with ICU support.
17041704

17051705
A non-context-aware native addon was loaded in a process that disallows them.
17061706

1707+
<a id="ERR_OPERATION_FAILED"></a>
1708+
### `ERR_OPERATION_FAILED`
1709+
1710+
An operation failed. This is typically used to signal the general failure of an
1711+
asynchronous operation.
1712+
17071713
<a id="ERR_OUT_OF_RANGE"></a>
17081714
### `ERR_OUT_OF_RANGE`
17091715

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,
@@ -186,6 +187,7 @@ module.exports = {
186187
randomFill,
187188
randomFillSync,
188189
randomInt,
190+
randomUUID,
189191
scrypt,
190192
scryptSync,
191193
sign: signOneShot,

lib/internal/crypto/random.js

+125-8
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@ const {
77
} = primordials;
88

99
const { AsyncWrap, Providers } = internalBinding('async_wrap');
10-
const { kMaxLength } = require('buffer');
11-
const { randomBytes: _randomBytes } = internalBinding('crypto');
1210
const {
13-
ERR_INVALID_ARG_TYPE,
14-
ERR_INVALID_CALLBACK,
15-
ERR_OUT_OF_RANGE
16-
} = require('internal/errors').codes;
17-
const { validateNumber } = require('internal/validators');
11+
Buffer,
12+
kMaxLength,
13+
} = require('buffer');
14+
const {
15+
randomBytes: _randomBytes,
16+
secureBuffer,
17+
} = internalBinding('crypto');
18+
const {
19+
codes: {
20+
ERR_INVALID_ARG_TYPE,
21+
ERR_INVALID_CALLBACK,
22+
ERR_OUT_OF_RANGE,
23+
ERR_OPERATION_FAILED,
24+
}
25+
} = require('internal/errors');
26+
const {
27+
validateBoolean,
28+
validateNumber,
29+
validateObject,
30+
} = require('internal/validators');
1831
const { isArrayBufferView } = require('internal/util/types');
1932
const { FastBuffer } = require('internal/buffer');
2033

@@ -203,9 +216,113 @@ function handleError(ex, buf) {
203216
return buf;
204217
}
205218

219+
// Implements an RFC 4122 version 4 random UUID.
220+
// To improve performance, random data is generated in batches
221+
// large enough to cover kBatchSize UUID's at a time. The uuidData
222+
// and uuid buffers are reused. Each call to randomUUID() consumes
223+
// 16 bytes from the buffer.
224+
225+
const kHexDigits = [
226+
48, 49, 50, 51, 52, 53, 54, 55,
227+
56, 57, 97, 98, 99, 100, 101, 102
228+
];
229+
230+
const kBatchSize = 128;
231+
let uuidData;
232+
let uuidNotBuffered;
233+
let uuid;
234+
let uuidBatch = 0;
235+
236+
function getBufferedUUID() {
237+
if (uuidData === undefined) {
238+
uuidData = secureBuffer(16 * kBatchSize);
239+
if (uuidData === undefined)
240+
throw new ERR_OPERATION_FAILED('Out of memory');
241+
}
242+
243+
if (uuidBatch === 0) randomFillSync(uuidData);
244+
uuidBatch = (uuidBatch + 1) % kBatchSize;
245+
return uuidData.slice(uuidBatch * 16, (uuidBatch * 16) + 16);
246+
}
247+
248+
function randomUUID(options) {
249+
if (options !== undefined)
250+
validateObject(options, 'options');
251+
const {
252+
disableEntropyCache = false,
253+
} = { ...options };
254+
255+
validateBoolean(disableEntropyCache, 'options.disableEntropyCache');
256+
257+
if (uuid === undefined) {
258+
uuid = Buffer.alloc(36, '-');
259+
uuid[14] = 52; // '4', identifies the UUID version
260+
}
261+
262+
let uuidBuf;
263+
if (!disableEntropyCache) {
264+
uuidBuf = getBufferedUUID();
265+
} else {
266+
uuidBuf = uuidNotBuffered;
267+
if (uuidBuf === undefined)
268+
uuidBuf = uuidNotBuffered = secureBuffer(16);
269+
if (uuidBuf === undefined)
270+
throw new ERR_OPERATION_FAILED('Out of memory');
271+
randomFillSync(uuidBuf);
272+
}
273+
274+
// Variant byte: 10xxxxxx (variant 1)
275+
uuidBuf[8] = (uuidBuf[8] & 0x3f) | 0x80;
276+
277+
// This function is structured the way it is for performance.
278+
// The uuid buffer stores the serialization of the random
279+
// bytes from uuidData.
280+
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
281+
let n = 0;
282+
uuid[0] = kHexDigits[uuidBuf[n] >> 4];
283+
uuid[1] = kHexDigits[uuidBuf[n++] & 0xf];
284+
uuid[2] = kHexDigits[uuidBuf[n] >> 4];
285+
uuid[3] = kHexDigits[uuidBuf[n++] & 0xf];
286+
uuid[4] = kHexDigits[uuidBuf[n] >> 4];
287+
uuid[5] = kHexDigits[uuidBuf[n++] & 0xf];
288+
uuid[6] = kHexDigits[uuidBuf[n] >> 4];
289+
uuid[7] = kHexDigits[uuidBuf[n++] & 0xf];
290+
// -
291+
uuid[9] = kHexDigits[uuidBuf[n] >> 4];
292+
uuid[10] = kHexDigits[uuidBuf[n++] & 0xf];
293+
uuid[11] = kHexDigits[uuidBuf[n] >> 4];
294+
uuid[12] = kHexDigits[uuidBuf[n++] & 0xf];
295+
// -
296+
// 4, uuid[14] is set already...
297+
uuid[15] = kHexDigits[uuidBuf[n++] & 0xf];
298+
uuid[16] = kHexDigits[uuidBuf[n] >> 4];
299+
uuid[17] = kHexDigits[uuidBuf[n++] & 0xf];
300+
// -
301+
uuid[19] = kHexDigits[uuidBuf[n] >> 4];
302+
uuid[20] = kHexDigits[uuidBuf[n++] & 0xf];
303+
uuid[21] = kHexDigits[uuidBuf[n] >> 4];
304+
uuid[22] = kHexDigits[uuidBuf[n++] & 0xf];
305+
// -
306+
uuid[24] = kHexDigits[uuidBuf[n] >> 4];
307+
uuid[25] = kHexDigits[uuidBuf[n++] & 0xf];
308+
uuid[26] = kHexDigits[uuidBuf[n] >> 4];
309+
uuid[27] = kHexDigits[uuidBuf[n++] & 0xf];
310+
uuid[28] = kHexDigits[uuidBuf[n] >> 4];
311+
uuid[29] = kHexDigits[uuidBuf[n++] & 0xf];
312+
uuid[30] = kHexDigits[uuidBuf[n] >> 4];
313+
uuid[31] = kHexDigits[uuidBuf[n++] & 0xf];
314+
uuid[32] = kHexDigits[uuidBuf[n] >> 4];
315+
uuid[33] = kHexDigits[uuidBuf[n++] & 0xf];
316+
uuid[34] = kHexDigits[uuidBuf[n] >> 4];
317+
uuid[35] = kHexDigits[uuidBuf[n] & 0xf];
318+
319+
return uuid.latin1Slice(0, 36);
320+
}
321+
206322
module.exports = {
207323
randomBytes,
208324
randomFill,
209325
randomFillSync,
210-
randomInt
326+
randomInt,
327+
randomUUID,
211328
};

lib/internal/errors.js

+1
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,7 @@ E('ERR_NO_CRYPTO',
12591259
'Node.js is not compiled with OpenSSL crypto support', Error);
12601260
E('ERR_NO_ICU',
12611261
'%s is not supported on Node.js compiled without ICU', TypeError);
1262+
E('ERR_OPERATION_FAILED', 'Operation failed: %s', Error);
12621263
E('ERR_OUT_OF_RANGE',
12631264
(str, range, input, replaceDefaultBoolean = false) => {
12641265
assert(range, 'Missing "range" argument');

src/node_crypto.cc

+34
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ namespace crypto {
6767
using node::THROW_ERR_TLS_INVALID_PROTOCOL_METHOD;
6868

6969
using v8::Array;
70+
using v8::ArrayBuffer;
7071
using v8::ArrayBufferView;
72+
using v8::BackingStore;
7173
using v8::Boolean;
7274
using v8::ConstructorBehavior;
7375
using v8::Context;
@@ -97,6 +99,7 @@ using v8::SideEffectType;
9799
using v8::Signature;
98100
using v8::String;
99101
using v8::Uint32;
102+
using v8::Uint8Array;
100103
using v8::Undefined;
101104
using v8::Value;
102105

@@ -6944,6 +6947,35 @@ void SetFipsCrypto(const FunctionCallbackInfo<Value>& args) {
69446947
}
69456948
#endif /* NODE_FIPS_MODE */
69466949

6950+
namespace {
6951+
// SecureBuffer uses openssl to allocate a Uint8Array using
6952+
// OPENSSL_secure_malloc. Because we do not yet actually
6953+
// make use of secure heap, this has the same semantics as
6954+
// using OPENSSL_malloc. However, if the secure heap is
6955+
// initialized, SecureBuffer will automatically use it.
6956+
void SecureBuffer(const FunctionCallbackInfo<Value>& args) {
6957+
CHECK(args[0]->IsUint32());
6958+
Environment* env = Environment::GetCurrent(args);
6959+
uint32_t len = args[0].As<Uint32>()->Value();
6960+
char* data = static_cast<char*>(OPENSSL_secure_malloc(len));
6961+
if (data == nullptr) {
6962+
// There's no memory available for the allocation.
6963+
// Return nothing.
6964+
return;
6965+
}
6966+
memset(data, 0, len);
6967+
std::shared_ptr<BackingStore> store =
6968+
ArrayBuffer::NewBackingStore(
6969+
data,
6970+
len,
6971+
[](void* data, size_t len, void* deleter_data) {
6972+
OPENSSL_secure_clear_free(data, len);
6973+
},
6974+
data);
6975+
Local<ArrayBuffer> buffer = ArrayBuffer::New(env->isolate(), store);
6976+
args.GetReturnValue().Set(Uint8Array::New(buffer, 0, len));
6977+
}
6978+
} // namespace
69476979

69486980
void Initialize(Local<Object> target,
69496981
Local<Value> unused,
@@ -7038,6 +7070,8 @@ void Initialize(Local<Object> target,
70387070
#ifndef OPENSSL_NO_SCRYPT
70397071
env->SetMethod(target, "scrypt", Scrypt);
70407072
#endif // OPENSSL_NO_SCRYPT
7073+
7074+
env->SetMethod(target, "secureBuffer", SecureBuffer);
70417075
}
70427076

70437077
} // namespace crypto
+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)