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‎

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

‎doc/api/crypto.md‎

Lines changed: 16 additions & 0 deletions
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‎

Lines changed: 3 additions & 1 deletion
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‎

Lines changed: 109 additions & 1 deletion
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
returndata;
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+
constkHexDigits=[
295+
48,49,50,51,52,53,54,55,
296+
56,57,97,98,99,100,101,102
297+
];
298+
299+
constkBatchSize=128;
300+
letuuidData;
301+
letuuidNotBuffered;
302+
letuuid;
303+
letuuidBatch=0;
304+
305+
functiongetBufferedUUID(){
306+
if(uuidData===undefined){
307+
uuidData=secureBuffer(16*kBatchSize);
308+
if(uuidData===undefined)
309+
thrownewERR_OPERATION_FAILED('Out of memory');
310+
}
311+
312+
if(uuidBatch===0)randomFillSync(uuidData);
313+
uuidBatch=(uuidBatch+1)%kBatchSize;
314+
returnuuidData.slice(uuidBatch*16,(uuidBatch*16)+16);
315+
}
316+
317+
functionrandomUUID(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+
letuuidBuf;
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+
thrownewERR_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+
letn=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+
returnuuid.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‎

Lines changed: 34 additions & 0 deletions
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
namespacecrypto{
@@ -587,6 +589,36 @@ CryptoJobMode GetCryptoJobMode(v8::Local<v8::Value> args){
587589
returnstatic_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+
voidSecureBuffer(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
namespaceUtil{
591623
voidInitialize(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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
5+
if(!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
constassert=require('assert');
9+
const{
10+
randomUUID,
11+
}=require('crypto');
12+
13+
constlast=newSet([
14+
'00000000-0000-0000-0000-000000000000'
15+
]);
16+
17+
functiontestMatch(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(letn=0;n<130;n++){
28+
constuuid=randomUUID();
29+
assert(!last.has(uuid));
30+
last.add(uuid);
31+
assert.strictEqual(typeofuuid,'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)