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}`);
|
||||
```
|
||||
|
||||
### `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)`
|
||||
<!-- YAML
|
||||
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 3610]: https://www.rfc-editor.org/rfc/rfc3610.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
|
||||
[Web Crypto API documentation]: webcrypto.md
|
||||
[`Buffer`]: buffer.md
|
||||
|
|
|
@ -53,7 +53,8 @@ const {
|
|||
randomBytes,
|
||||
randomFill,
|
||||
randomFillSync,
|
||||
randomInt
|
||||
randomInt,
|
||||
randomUUID,
|
||||
} = require('internal/crypto/random');
|
||||
const {
|
||||
pbkdf2,
|
||||
|
@ -199,6 +200,7 @@ module.exports = {
|
|||
randomFill,
|
||||
randomFillSync,
|
||||
randomInt,
|
||||
randomUUID,
|
||||
scrypt,
|
||||
scryptSync,
|
||||
sign: signOneShot,
|
||||
|
|
|
@ -12,24 +12,28 @@ const {
|
|||
RandomBytesJob,
|
||||
kCryptoJobAsync,
|
||||
kCryptoJobSync,
|
||||
secureBuffer,
|
||||
} = internalBinding('crypto');
|
||||
|
||||
const {
|
||||
lazyDOMException,
|
||||
} = require('internal/crypto/util');
|
||||
|
||||
const { kMaxLength } = require('buffer');
|
||||
const { Buffer, kMaxLength } = require('buffer');
|
||||
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
ERR_OUT_OF_RANGE,
|
||||
ERR_OPERATION_FAILED,
|
||||
}
|
||||
} = require('internal/errors');
|
||||
|
||||
const {
|
||||
validateNumber,
|
||||
validateBoolean,
|
||||
validateCallback,
|
||||
validateObject,
|
||||
} = require('internal/validators');
|
||||
|
||||
const {
|
||||
|
@ -281,10 +285,114 @@ function getRandomValues(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 = {
|
||||
randomBytes,
|
||||
randomFill,
|
||||
randomFillSync,
|
||||
randomInt,
|
||||
getRandomValues,
|
||||
randomUUID,
|
||||
};
|
||||
|
|
|
@ -31,6 +31,8 @@ using v8::NewStringType;
|
|||
using v8::Nothing;
|
||||
using v8::Object;
|
||||
using v8::String;
|
||||
using v8::Uint32;
|
||||
using v8::Uint8Array;
|
||||
using v8::Value;
|
||||
|
||||
namespace crypto {
|
||||
|
@ -587,6 +589,36 @@ CryptoJobMode GetCryptoJobMode(v8::Local<v8::Value> args) {
|
|||
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 {
|
||||
void Initialize(Environment* env, Local<Object> target) {
|
||||
#ifndef OPENSSL_NO_ENGINE
|
||||
|
@ -600,6 +632,8 @@ void Initialize(Environment* env, Local<Object> target) {
|
|||
|
||||
NODE_DEFINE_CONSTANT(target, kCryptoJobAsync);
|
||||
NODE_DEFINE_CONSTANT(target, kCryptoJobSync);
|
||||
|
||||
env->SetMethod(target, "secureBuffer", SecureBuffer);
|
||||
}
|
||||
} // 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