GeezerVison is showing github.com — crypto: implement randomuuid · nodejs/node@08f9130
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboard +17Lines changed: 17 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common.js'); | | 4 | +const { randomUUID } = require('crypto'); | | 5 | + | | 6 | +const bench = common.createBenchmark(main, { | | 7 | + n: [1e7], | | 8 | + disableEntropyCache: [0, 1], | | 9 | +}); | | 10 | + | | 11 | +function main({ n, disableEntropyCache }) { | | 12 | + disableEntropyCache = !!disableEntropyCache; | | 13 | + bench.start(); | | 14 | + for (let i = 0; i < n; ++i) | | 15 | + randomUUID({ disableEntropyCache }); | | 16 | + bench.end(n); | | 17 | +} |
Collapse file Copy file name to clipboardExpand all lines: doc/api/crypto.md +15Lines changed: 15 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2841,6 +2841,20 @@ const n = crypto.randomInt(1, 7); | 2841 | 2841 | console.log(`The dice rolled: ${n}`); | 2842 | 2842 | ``` | 2843 | 2843 |
| | 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 | + | 2844 | 2858 | ### `crypto.scrypt(password, salt, keylen[, options], callback)` | 2845 | 2859 | <!-- YAML | 2846 | 2860 | added: v10.5.0 | @@ -3582,6 +3596,7 @@ See the [list of SSL OP Flags][] for details. | 3582 | 3596 | [RFC 3526]: https://www.rfc-editor.org/rfc/rfc3526.txt | 3583 | 3597 | [RFC 3610]: https://www.rfc-editor.org/rfc/rfc3610.txt | 3584 | 3598 | [RFC 4055]: https://www.rfc-editor.org/rfc/rfc4055.txt | | 3599 | +[RFC 4122]: https://www.rfc-editor.org/rfc/rfc4122.txt | 3585 | 3600 | [RFC 5208]: https://www.rfc-editor.org/rfc/rfc5208.txt | 3586 | 3601 | [`Buffer`]: buffer.md | 3587 | 3602 | [`EVP_BytesToKey`]: https://www.openssl.org/docs/man1.1.0/crypto/EVP_BytesToKey.html | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/errors.md +6Lines changed: 6 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1704,6 +1704,12 @@ compiled with ICU support. | 1704 | 1704 |
| 1705 | 1705 | A non-context-aware native addon was loaded in a process that disallows them. | 1706 | 1706 |
| | 1707 | +<a id="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 | + | 1707 | 1713 | <a id="ERR_OUT_OF_RANGE"></a> | 1708 | 1714 | ### `ERR_OUT_OF_RANGE` | 1709 | 1715 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/crypto.js +3-1Lines changed: 3 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -53,7 +53,8 @@ const { | 53 | 53 | randomBytes, | 54 | 54 | randomFill, | 55 | 55 | randomFillSync, | 56 | | - randomInt | | 56 | + randomInt, | | 57 | + randomUUID, | 57 | 58 | } = require('internal/crypto/random'); | 58 | 59 | const { | 59 | 60 | pbkdf2, | @@ -186,6 +187,7 @@ module.exports = { | 186 | 187 | randomFill, | 187 | 188 | randomFillSync, | 188 | 189 | randomInt, | | 190 | + randomUUID, | 189 | 191 | scrypt, | 190 | 192 | scryptSync, | 191 | 193 | sign: signOneShot, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/crypto/random.js +125-8Lines changed: 125 additions & 8 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -7,14 +7,27 @@ const { | 7 | 7 | } = primordials; | 8 | 8 |
| 9 | 9 | const { AsyncWrap, Providers } = internalBinding('async_wrap'); | 10 | | -const { kMaxLength } = require('buffer'); | 11 | | -const { randomBytes: _randomBytes } = internalBinding('crypto'); | 12 | 10 | 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'); | 18 | 31 | const { isArrayBufferView } = require('internal/util/types'); | 19 | 32 | const { FastBuffer } = require('internal/buffer'); | 20 | 33 |
| @@ -203,9 +216,113 @@ function handleError(ex, buf) { | 203 | 216 | return buf; | 204 | 217 | } | 205 | 218 |
| | 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 | +const kHexDigits = [ | | 226 | + 48, 49, 50, 51, 52, 53, 54, 55, | | 227 | + 56, 57, 97, 98, 99, 100, 101, 102 | | 228 | +]; | | 229 | + | | 230 | +const kBatchSize = 128; | | 231 | +let uuidData; | | 232 | +let uuidNotBuffered; | | 233 | +let uuid; | | 234 | +let uuidBatch = 0; | | 235 | + | | 236 | +function getBufferedUUID() { | | 237 | + if (uuidData === undefined) { | | 238 | + uuidData = secureBuffer(16 * kBatchSize); | | 239 | + if (uuidData === undefined) | | 240 | + throw new ERR_OPERATION_FAILED('Out of memory'); | | 241 | + } | | 242 | + | | 243 | + if (uuidBatch === 0) randomFillSync(uuidData); | | 244 | + uuidBatch = (uuidBatch + 1) % kBatchSize; | | 245 | + return uuidData.slice(uuidBatch * 16, (uuidBatch * 16) + 16); | | 246 | +} | | 247 | + | | 248 | +function randomUUID(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 | + let uuidBuf; | | 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 | + throw new ERR_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 | + let n = 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 | + return uuid.latin1Slice(0, 36); | | 320 | +} | | 321 | + | 206 | 322 | module.exports = { | 207 | 323 | randomBytes, | 208 | 324 | randomFill, | 209 | 325 | randomFillSync, | 210 | | - randomInt | | 326 | + randomInt, | | 327 | + randomUUID, | 211 | 328 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/errors.js +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1259,6 +1259,7 @@ E('ERR_NO_CRYPTO', | 1259 | 1259 | 'Node.js is not compiled with OpenSSL crypto support', Error); | 1260 | 1260 | E('ERR_NO_ICU', | 1261 | 1261 | '%s is not supported on Node.js compiled without ICU', TypeError); | | 1262 | +E('ERR_OPERATION_FAILED', 'Operation failed: %s', Error); | 1262 | 1263 | E('ERR_OUT_OF_RANGE', | 1263 | 1264 | (str, range, input, replaceDefaultBoolean = false) => { | 1264 | 1265 | assert(range, 'Missing "range" argument'); | |
Collapse file Copy file name to clipboardExpand all lines: src/node_crypto.cc +34Lines changed: 34 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -67,7 +67,9 @@ namespace crypto { | 67 | 67 | using node::THROW_ERR_TLS_INVALID_PROTOCOL_METHOD; | 68 | 68 |
| 69 | 69 | using v8::Array; | | 70 | +using v8::ArrayBuffer; | 70 | 71 | using v8::ArrayBufferView; | | 72 | +using v8::BackingStore; | 71 | 73 | using v8::Boolean; | 72 | 74 | using v8::ConstructorBehavior; | 73 | 75 | using v8::Context; | @@ -97,6 +99,7 @@ using v8::SideEffectType; | 97 | 99 | using v8::Signature; | 98 | 100 | using v8::String; | 99 | 101 | using v8::Uint32; | | 102 | +using v8::Uint8Array; | 100 | 103 | using v8::Undefined; | 101 | 104 | using v8::Value; | 102 | 105 |
| @@ -6944,6 +6947,35 @@ void SetFipsCrypto(const FunctionCallbackInfo<Value>& args) { | 6944 | 6947 | } | 6945 | 6948 | #endif /* NODE_FIPS_MODE */ | 6946 | 6949 |
| | 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 | +void SecureBuffer(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 | 6947 | 6979 |
| 6948 | 6980 | void Initialize(Local<Object> target, | 6949 | 6981 | Local<Value> unused, | @@ -7038,6 +7070,8 @@ void Initialize(Local<Object> target, | 7038 | 7070 | #ifndef OPENSSL_NO_SCRYPT | 7039 | 7071 | env->SetMethod(target, "scrypt", Scrypt); | 7040 | 7072 | #endif // OPENSSL_NO_SCRYPT | | 7073 | + | | 7074 | + env->SetMethod(target, "secureBuffer", SecureBuffer); | 7041 | 7075 | } | 7042 | 7076 |
| 7043 | 7077 | } // namespace crypto | |
Collapse file Copy file name to clipboard +58Lines changed: 58 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | + | | 3 | +const common = require('../common'); | | 4 | + | | 5 | +if (!common.hasCrypto) | | 6 | + common.skip('missing crypto'); | | 7 | + | | 8 | +const assert = require('assert'); | | 9 | +const { | | 10 | + randomUUID, | | 11 | +} = require('crypto'); | | 12 | + | | 13 | +const last = new Set([ | | 14 | + '00000000-0000-0000-0000-000000000000' | | 15 | +]); | | 16 | + | | 17 | +function testMatch(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 (let n = 0; n < 130; n++) { | | 28 | + const uuid = randomUUID(); | | 29 | + assert(!last.has(uuid)); | | 30 | + last.add(uuid); | | 31 | + assert.strictEqual(typeof uuid, '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 | +} |
|