Skip to content

Commit 08f9130

Browse files
jasnelltargos
authored andcommitted
crypto: implement randomuuid
Signed-off-by: James M Snell <[email protected]> PR-URL: #36729 Backport-PR-URL: #36945 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 7c9f3a9 commit 08f9130

File tree

8 files changed

+259
-9
lines changed

8 files changed

+259
-9
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: 15 additions & 0 deletions
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‎

Lines changed: 6 additions & 0 deletions
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+
<aid="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
<aid="ERR_OUT_OF_RANGE"></a>
17081714
### `ERR_OUT_OF_RANGE`
17091715

‎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,
@@ -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‎

Lines changed: 125 additions & 8 deletions
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
returnbuf;
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+
constkHexDigits=[
226+
48,49,50,51,52,53,54,55,
227+
56,57,97,98,99,100,101,102
228+
];
229+
230+
constkBatchSize=128;
231+
letuuidData;
232+
letuuidNotBuffered;
233+
letuuid;
234+
letuuidBatch=0;
235+
236+
functiongetBufferedUUID(){
237+
if(uuidData===undefined){
238+
uuidData=secureBuffer(16*kBatchSize);
239+
if(uuidData===undefined)
240+
thrownewERR_OPERATION_FAILED('Out of memory');
241+
}
242+
243+
if(uuidBatch===0)randomFillSync(uuidData);
244+
uuidBatch=(uuidBatch+1)%kBatchSize;
245+
returnuuidData.slice(uuidBatch*16,(uuidBatch*16)+16);
246+
}
247+
248+
functionrandomUUID(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+
letuuidBuf;
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+
thrownewERR_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+
letn=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+
returnuuid.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‎

Lines changed: 1 addition & 0 deletions
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‎

Lines changed: 34 additions & 0 deletions
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+
voidSecureBuffer(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
voidInitialize(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
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)