Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v14.x Backport] crypto: implement randomuuid #36945

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions benchmark/crypto/randomUUID.js
Original file line number Diff line number Diff line change
@@ -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);
}
15 changes: 15 additions & 0 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2841,6 +2841,20 @@ 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 generates and caches 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
Expand Down Expand Up @@ -3582,6 +3596,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
[`Buffer`]: buffer.md
[`EVP_BytesToKey`]: https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html
Expand Down
6 changes: 6 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1704,6 +1704,12 @@ compiled with ICU support.

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

<a id="ERR_OPERATION_FAILED"></a>
### `ERR_OPERATION_FAILED`

An operation failed. This is typically used to signal the general failure of an
asynchronous operation.

<a id="ERR_OUT_OF_RANGE"></a>
### `ERR_OUT_OF_RANGE`

Expand Down
4 changes: 3 additions & 1 deletion lib/crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ const {
randomBytes,
randomFill,
randomFillSync,
randomInt
randomInt,
randomUUID,
} = require('internal/crypto/random');
const {
pbkdf2,
Expand Down Expand Up @@ -186,6 +187,7 @@ module.exports = {
randomFill,
randomFillSync,
randomInt,
randomUUID,
scrypt,
scryptSync,
sign: signOneShot,
Expand Down
133 changes: 125 additions & 8 deletions lib/internal/crypto/random.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@ const {
} = primordials;

const { AsyncWrap, Providers } = internalBinding('async_wrap');
const { kMaxLength } = require('buffer');
const { randomBytes: _randomBytes } = internalBinding('crypto');
const {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_CALLBACK,
ERR_OUT_OF_RANGE
} = require('internal/errors').codes;
const { validateNumber } = require('internal/validators');
Buffer,
kMaxLength,
} = require('buffer');
const {
randomBytes: _randomBytes,
secureBuffer,
} = internalBinding('crypto');
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_CALLBACK,
ERR_OUT_OF_RANGE,
ERR_OPERATION_FAILED,
}
} = require('internal/errors');
const {
validateBoolean,
validateNumber,
validateObject,
} = require('internal/validators');
const { isArrayBufferView } = require('internal/util/types');
const { FastBuffer } = require('internal/buffer');

Expand Down Expand Up @@ -203,9 +216,113 @@ function handleError(ex, buf) {
return buf;
}

// 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
randomInt,
randomUUID,
};
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,7 @@ E('ERR_NO_CRYPTO',
'Node.js is not compiled with OpenSSL crypto support', Error);
E('ERR_NO_ICU',
'%s is not supported on Node.js compiled without ICU', TypeError);
E('ERR_OPERATION_FAILED', 'Operation failed: %s', Error);
E('ERR_OUT_OF_RANGE',
(str, range, input, replaceDefaultBoolean = false) => {
assert(range, 'Missing "range" argument');
Expand Down
34 changes: 34 additions & 0 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ namespace crypto {
using node::THROW_ERR_TLS_INVALID_PROTOCOL_METHOD;

using v8::Array;
using v8::ArrayBuffer;
using v8::ArrayBufferView;
using v8::BackingStore;
using v8::Boolean;
using v8::ConstructorBehavior;
using v8::Context;
Expand Down Expand Up @@ -97,6 +99,7 @@ using v8::SideEffectType;
using v8::Signature;
using v8::String;
using v8::Uint32;
using v8::Uint8Array;
using v8::Undefined;
using v8::Value;

Expand Down Expand Up @@ -6944,6 +6947,35 @@ void SetFipsCrypto(const FunctionCallbackInfo<Value>& args) {
}
#endif /* NODE_FIPS_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

void Initialize(Local<Object> target,
Local<Value> unused,
Expand Down Expand Up @@ -7038,6 +7070,8 @@ void Initialize(Local<Object> target,
#ifndef OPENSSL_NO_SCRYPT
env->SetMethod(target, "scrypt", Scrypt);
#endif // OPENSSL_NO_SCRYPT

env->SetMethod(target, "secureBuffer", SecureBuffer);
}

} // namespace crypto
Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-crypto-randomuuid.js
Original file line number Diff line number Diff line change
@@ -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'
});
}