mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
crypto: implement randomuuid
Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: https://github.com/nodejs/node/pull/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>
This commit is contained in:
parent
9ff555b7a2
commit
0008a675ff
6 changed files with 237 additions and 2 deletions
17
benchmark/crypto/randomUUID.js
Normal file
17
benchmark/crypto/randomUUID.js
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -3160,6 +3160,21 @@ const n = crypto.randomInt(1, 7);
|
||||||
console.log(`The dice rolled: ${n}`);
|
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)`
|
### `crypto.scrypt(password, salt, keylen[, options], callback)`
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
added: v10.5.0
|
added: v10.5.0
|
||||||
|
@ -3929,6 +3944,7 @@ See the [list of SSL OP Flags][] for details.
|
||||||
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
|
[RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt
|
||||||
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
|
[RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt
|
||||||
[RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.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
|
[RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt
|
||||||
[Web Crypto API documentation]: webcrypto.md
|
[Web Crypto API documentation]: webcrypto.md
|
||||||
[`Buffer`]: buffer.md
|
[`Buffer`]: buffer.md
|
||||||
|
|
|
@ -53,7 +53,8 @@ const {
|
||||||
randomBytes,
|
randomBytes,
|
||||||
randomFill,
|
randomFill,
|
||||||
randomFillSync,
|
randomFillSync,
|
||||||
randomInt
|
randomInt,
|
||||||
|
randomUUID,
|
||||||
} = require('internal/crypto/random');
|
} = require('internal/crypto/random');
|
||||||
const {
|
const {
|
||||||
pbkdf2,
|
pbkdf2,
|
||||||
|
@ -199,6 +200,7 @@ module.exports = {
|
||||||
randomFill,
|
randomFill,
|
||||||
randomFillSync,
|
randomFillSync,
|
||||||
randomInt,
|
randomInt,
|
||||||
|
randomUUID,
|
||||||
scrypt,
|
scrypt,
|
||||||
scryptSync,
|
scryptSync,
|
||||||
sign: signOneShot,
|
sign: signOneShot,
|
||||||
|
|
|
@ -12,24 +12,28 @@ const {
|
||||||
RandomBytesJob,
|
RandomBytesJob,
|
||||||
kCryptoJobAsync,
|
kCryptoJobAsync,
|
||||||
kCryptoJobSync,
|
kCryptoJobSync,
|
||||||
|
secureBuffer,
|
||||||
} = internalBinding('crypto');
|
} = internalBinding('crypto');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
lazyDOMException,
|
lazyDOMException,
|
||||||
} = require('internal/crypto/util');
|
} = require('internal/crypto/util');
|
||||||
|
|
||||||
const { kMaxLength } = require('buffer');
|
const { Buffer, kMaxLength } = require('buffer');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
codes: {
|
codes: {
|
||||||
ERR_INVALID_ARG_TYPE,
|
ERR_INVALID_ARG_TYPE,
|
||||||
ERR_OUT_OF_RANGE,
|
ERR_OUT_OF_RANGE,
|
||||||
|
ERR_OPERATION_FAILED,
|
||||||
}
|
}
|
||||||
} = require('internal/errors');
|
} = require('internal/errors');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
validateNumber,
|
validateNumber,
|
||||||
|
validateBoolean,
|
||||||
validateCallback,
|
validateCallback,
|
||||||
|
validateObject,
|
||||||
} = require('internal/validators');
|
} = require('internal/validators');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -281,10 +285,114 @@ function getRandomValues(data) {
|
||||||
return 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 = {
|
module.exports = {
|
||||||
randomBytes,
|
randomBytes,
|
||||||
randomFill,
|
randomFill,
|
||||||
randomFillSync,
|
randomFillSync,
|
||||||
randomInt,
|
randomInt,
|
||||||
getRandomValues,
|
getRandomValues,
|
||||||
|
randomUUID,
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,8 @@ using v8::NewStringType;
|
||||||
using v8::Nothing;
|
using v8::Nothing;
|
||||||
using v8::Object;
|
using v8::Object;
|
||||||
using v8::String;
|
using v8::String;
|
||||||
|
using v8::Uint32;
|
||||||
|
using v8::Uint8Array;
|
||||||
using v8::Value;
|
using v8::Value;
|
||||||
|
|
||||||
namespace crypto {
|
namespace crypto {
|
||||||
|
@ -587,6 +589,36 @@ CryptoJobMode GetCryptoJobMode(v8::Local<v8::Value> args) {
|
||||||
return static_cast<CryptoJobMode>(mode);
|
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 {
|
namespace Util {
|
||||||
void Initialize(Environment* env, Local<Object> target) {
|
void Initialize(Environment* env, Local<Object> target) {
|
||||||
#ifndef OPENSSL_NO_ENGINE
|
#ifndef OPENSSL_NO_ENGINE
|
||||||
|
@ -600,6 +632,8 @@ void Initialize(Environment* env, Local<Object> target) {
|
||||||
|
|
||||||
NODE_DEFINE_CONSTANT(target, kCryptoJobAsync);
|
NODE_DEFINE_CONSTANT(target, kCryptoJobAsync);
|
||||||
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
|
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
|
||||||
|
|
||||||
|
env->SetMethod(target, "secureBuffer", SecureBuffer);
|
||||||
}
|
}
|
||||||
} // namespace Util
|
} // namespace Util
|
||||||
|
|
||||||
|
|
58
test/parallel/test-crypto-randomuuid.js
Normal file
58
test/parallel/test-crypto-randomuuid.js
Normal file
|
@ -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'
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue