Skip to content

Commit

Permalink
crypto: implement randomuuid
Browse files Browse the repository at this point in the history
Signed-off-by: James M Snell <jasnell@gmail.com>
Original-PR-URL: nodejs#36729
  • Loading branch information
jasnell committed Jan 15, 2021
1 parent 5b5bed9 commit 1b3860c
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 9 deletions.
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'
});
}

0 comments on commit 1b3860c

Please sign in to comment.