Skip to content
File

Blob: src/node/internal/crypto_random.ts

typescript473 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import { default as cryptoImpl } from 'node-internal:crypto';
27 
28import {
29 validateObject,
30 validateBoolean,
31 validateFunction,
32 validateInt32,
33 validateInteger,
34} from 'node-internal:validators';
35 
36import {
37 isAnyArrayBuffer,
38 isArrayBufferView,
39} from 'node-internal:internal_types';
40 
41import {
42 ERR_INVALID_ARG_TYPE,
43 ERR_OUT_OF_RANGE,
44} from 'node-internal:internal_errors';
45 
46import { Buffer, kMaxLength } from 'node-internal:internal_buffer';
47 
48import { arrayBufferToUnsignedBigInt } from 'node-internal:crypto_util';
49import type { RandomUUIDOptions } from 'node:crypto';
50 
51export type RandomBytesCallback = (
52 err: Error | null,
53 buffer: Uint8Array
54) => void;
55export function randomBytes(size: number, callback: RandomBytesCallback): void;
56export function randomBytes(size: number): Uint8Array;
57export function randomBytes(
58 size: number,
59 callback?: RandomBytesCallback
60): Uint8Array | undefined {
61 validateInteger(size, 'size', 0, kMaxLength);
62 const buf = Buffer.alloc(size);
63 if (callback !== undefined) {
64 randomFill(buf, callback as RandomFillCallback);
65 return undefined;
66 } else {
67 randomFillSync(buf);
68 return buf;
69 }
70}
71 
72export function randomFillSync(
73 buffer: NodeJS.ArrayBufferView,
74 offset?: number,
75 size?: number
76): Uint8Array {
77 if (!isAnyArrayBuffer(buffer) && !isArrayBufferView(buffer)) {
78 throw new ERR_INVALID_ARG_TYPE(
79 'buffer',
80 ['TypedArray', 'DataView', 'ArrayBuffer', 'SharedArrayBuffer'],
81 buffer
82 );
83 }
84 const maxLength = (buffer as Uint8Array).length;
85 if (offset !== undefined) {
86 validateInteger(offset, 'offset', 0, kMaxLength);
87 } else offset = 0;
88 if (size !== undefined) {
89 validateInteger(size, 'size', 0, maxLength - offset);
90 } else size = maxLength - offset;
91 if (isAnyArrayBuffer(buffer)) {
92 buffer = Buffer.from(buffer);
93 }
94 buffer = (buffer as Buffer).subarray(offset, offset + size);
95 return crypto.getRandomValues(buffer as Uint8Array<ArrayBuffer>);
96}
97 
98export type RandomFillCallback = (
99 err: Error | null,
100 buf?: NodeJS.ArrayBufferView
101) => void;
102export function randomFill(
103 buffer: NodeJS.ArrayBufferView,
104 callback?: RandomFillCallback
105): void;
106export function randomFill(
107 buffer: NodeJS.ArrayBufferView,
108 offset: number,
109 callback?: RandomFillCallback
110): void;
111export function randomFill(
112 buffer: NodeJS.ArrayBufferView,
113 offset: number,
114 size: number,
115 callback?: RandomFillCallback
116): void;
117export function randomFill(
118 buffer: NodeJS.ArrayBufferView,
119 offsetOrCallback?: number | RandomFillCallback,
120 sizeOrCallback?: number | RandomFillCallback,
121 callback?: RandomFillCallback
122): void {
123 if (!isAnyArrayBuffer(buffer) && !isArrayBufferView(buffer)) {
124 throw new ERR_INVALID_ARG_TYPE(
125 'buffer',
126 ['TypedArray', 'DataView', 'ArrayBuffer', 'SharedArrayBuffer'],
127 buffer
128 );
129 }
130 
131 let offset = 0;
132 let size = 0;
133 const maxLength = (buffer as Uint8Array).length;
134 if (typeof callback === 'function') {
135 validateInteger(offsetOrCallback, 'offset', 0, maxLength);
136 offset = offsetOrCallback;
137 
138 validateInteger(sizeOrCallback, 'size', 0, maxLength - offset);
139 size = sizeOrCallback;
140 } else if (typeof sizeOrCallback === 'function') {
141 validateInteger(offsetOrCallback, 'offset', 0, maxLength);
142 offset = offsetOrCallback;
143 size = maxLength - offset;
144 callback = sizeOrCallback;
145 } else if (typeof offsetOrCallback === 'function') {
146 offset = 0;
147 size = maxLength;
148 callback = offsetOrCallback;
149 }
150 validateFunction(callback, 'callback');
151 
152 // We're currently not actually implementing the fill itself asynchronously,
153 // so we defer to randomFillSync here, but we invoke the callback asynchronously.
154 new Promise<void>((res) => {
155 randomFillSync(buffer, offset, size);
156 res();
157 }).then(
158 (): unknown => callback(null, buffer),
159 (err: unknown): unknown => callback(err as Error)
160 );
161}
162 
163const RAND_MAX = 0xffff_ffff_ffff;
164// Cache random data to use in randomInt. The cache size must be evenly
165// divisible by 6 because each attempt to obtain a random int uses 6 bytes.
166const randomCache = Buffer.alloc(6 * 1024);
167let randomCacheOffset = 0;
168let initialized = false;
169 
170function getRandomInt(min: number, max: number): number {
171 if (!initialized) {
172 randomFillSync(randomCache);
173 initialized = true;
174 }
175 // First we generate a random int between [0..range)
176 const range = max - min;
177 
178 if (!(range <= RAND_MAX)) {
179 throw new ERR_OUT_OF_RANGE(
180 `max${max ? '' : ' - min'}`,
181 `<= ${RAND_MAX}`,
182 range
183 );
184 }
185 
186 // For (x % range) to produce an unbiased value greater than or equal to 0 and
187 // less than range, x must be drawn randomly from the set of integers greater
188 // than or equal to 0 and less than randLimit.
189 const randLimit = RAND_MAX - (RAND_MAX % range);
190 
191 // If we don't have a callback, or if there is still data in the cache, we can
192 // do this synchronously, which is super fast.
193 while (randomCacheOffset <= randomCache.length) {
194 if (randomCacheOffset === randomCache.length) {
195 // This might block the thread for a bit, but we are in sync mode.
196 randomFillSync(randomCache);
197 randomCacheOffset = 0;
198 }
199 
200 const x = randomCache.readUIntBE(randomCacheOffset, 6);
201 randomCacheOffset += 6;
202 if (x < randLimit) {
203 return (x % range) + min;
204 }
205 }
206 return 0; // Should be unreachable.
207}
208 
209export type RandomIntCallback = (err: Error | null, n?: number) => void;
210export function randomInt(max: number): number;
211export function randomInt(min: number, max: number): number;
212export function randomInt(max: number, callback: RandomIntCallback): void;
213export function randomInt(
214 min: number,
215 max: number,
216 callback: RandomIntCallback
217): void;
218export function randomInt(
219 minOrMax: number,
220 maxOrCallback?: number | RandomIntCallback,
221 callback?: RandomIntCallback
222): number | undefined {
223 let min = 0;
224 let max = 0;
225 if (typeof callback === 'function') {
226 validateInteger(minOrMax, 'min');
227 validateInteger(maxOrCallback, 'max');
228 min = minOrMax;
229 max = maxOrCallback;
230 } else if (typeof maxOrCallback === 'function') {
231 min = 0;
232 validateInteger(minOrMax, 'max');
233 max = minOrMax;
234 callback = maxOrCallback;
235 } else if (arguments.length === 2) {
236 validateInteger(minOrMax, 'min');
237 validateInteger(maxOrCallback, 'max');
238 min = minOrMax;
239 max = maxOrCallback;
240 } else {
241 min = 0;
242 validateInteger(minOrMax, 'max');
243 max = minOrMax;
244 }
245 
246 if (min >= max) {
247 throw new ERR_OUT_OF_RANGE('min', 'min < max', min);
248 }
249 
250 if (callback != null) {
251 new Promise<number>((res) => {
252 res(getRandomInt(min, max));
253 }).then(
254 (n: number): void => {
255 callback(null, n);
256 },
257 (err: unknown): void => {
258 callback(err as Error);
259 }
260 );
261 } else {
262 return getRandomInt(min, max);
263 }
264 
265 return undefined;
266}
267 
268export function randomUUID(options?: RandomUUIDOptions): string {
269 // While we do not actually use the entropy cache, we go ahead and validate
270 // the input parameters as Node.js does.
271 if (options !== undefined) {
272 validateObject(options, 'options');
273 if (options.disableEntropyCache !== undefined) {
274 validateBoolean(
275 options.disableEntropyCache,
276 'options.disableEntropyCache'
277 );
278 }
279 }
280 return crypto.randomUUID();
281}
282 
283export type PrimeNum = ArrayBuffer | ArrayBufferView | Buffer | bigint;
284export interface GeneratePrimeOptions {
285 add?: PrimeNum;
286 rem?: PrimeNum;
287 safe?: boolean;
288 bigint?: boolean;
289}
290 
291export interface CheckPrimeOptions {
292 checks?: number;
293}
294 
295export type GeneratePrimeCallback = (
296 err: Error | null,
297 prime?: bigint | ArrayBuffer
298) => void;
299export type CheckPrimeCallback = (err: Error | null, prime?: boolean) => void;
300 
301function processGeneratePrimeOptions(options: GeneratePrimeOptions): {
302 add: ArrayBufferView;
303 rem: ArrayBufferView;
304 safe: boolean;
305 bigint: boolean;
306} {
307 validateObject(options, 'options');
308 const { safe = false, bigint = false } = options;
309 let { add, rem } = options;
310 validateBoolean(safe, 'options.safe');
311 validateBoolean(bigint, 'options.bigint');
312 
313 if (add !== undefined) {
314 if (typeof add === 'bigint') {
315 add = unsignedBigIntToBuffer(add, 'options.add');
316 } else if (!isAnyArrayBuffer(add) && !isArrayBufferView(add)) {
317 throw new ERR_INVALID_ARG_TYPE(
318 'options.add',
319 ['ArrayBuffer', 'TypedArray', 'Buffer', 'DataView', 'bigint'],
320 add
321 );
322 }
323 }
324 
325 if (rem !== undefined) {
326 if (typeof rem === 'bigint') {
327 rem = unsignedBigIntToBuffer(rem, 'options.rem');
328 } else if (!isAnyArrayBuffer(rem) && !isArrayBufferView(rem)) {
329 throw new ERR_INVALID_ARG_TYPE(
330 'options.rem',
331 ['ArrayBuffer', 'TypedArray', 'Buffer', 'DataView', 'bigint'],
332 rem
333 );
334 }
335 }
336 
337 return {
338 safe,
339 bigint,
340 add: add as ArrayBufferView,
341 rem: rem as ArrayBufferView,
342 };
343}
344 
345export function generatePrimeSync(
346 size: number,
347 options: GeneratePrimeOptions = {}
348): bigint | ArrayBuffer {
349 validateInt32(size, 'size', 1);
350 const { safe, bigint, add, rem } = processGeneratePrimeOptions(options);
351 
352 const primeBuf = cryptoImpl.randomPrime(size, safe, add, rem);
353 return bigint ? arrayBufferToUnsignedBigInt(primeBuf) : primeBuf;
354}
355 
356export function generatePrime(
357 size: number,
358 options: GeneratePrimeOptions,
359 callback: GeneratePrimeCallback
360): void;
361export function generatePrime(
362 size: number,
363 callback: GeneratePrimeCallback
364): void;
365export function generatePrime(
366 size: number,
367 options: GeneratePrimeOptions | GeneratePrimeCallback,
368 callback?: GeneratePrimeCallback
369): void {
370 validateInt32(size, 'size', 1);
371 if (typeof options === 'function') {
372 callback = options;
373 options = {};
374 }
375 validateFunction(callback, 'callback');
376 
377 const { safe, bigint, add, rem } = processGeneratePrimeOptions(options);
378 
379 new Promise<bigint | ArrayBuffer>((res, rej) => {
380 try {
381 const primeBuf = cryptoImpl.randomPrime(size, safe, add, rem);
382 res(bigint ? arrayBufferToUnsignedBigInt(primeBuf) : primeBuf);
383 } catch (err) {
384 rej(err as Error);
385 }
386 }).then(
387 (val: bigint | ArrayBuffer): void => {
388 callback(null, val);
389 },
390 (err: unknown): void => {
391 callback(err as Error);
392 }
393 );
394}
395 
396function unsignedBigIntToBuffer(bigint: bigint, name: string): Buffer {
397 if (bigint < 0) {
398 throw new ERR_OUT_OF_RANGE(name, '>= 0', bigint);
399 }
400 
401 const hex = bigint.toString(16);
402 const padded = hex.padStart(hex.length + (hex.length % 2), '0');
403 return Buffer.from(padded, 'hex');
404}
405 
406function validateCandidate(candidate: PrimeNum): Buffer {
407 if (typeof candidate === 'bigint')
408 candidate = unsignedBigIntToBuffer(candidate, 'candidate');
409 if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) {
410 throw new ERR_INVALID_ARG_TYPE(
411 'candidate',
412 ['ArrayBuffer', 'TypedArray', 'Buffer', 'DataView', 'bigint'],
413 candidate
414 );
415 }
416 return candidate as Buffer;
417}
418 
419function validateChecks(options: CheckPrimeOptions): number {
420 const { checks = 0 } = options;
421 // The checks option is unsigned but must fit into a signed 32-bit integer for OpenSSL.
422 validateInt32(checks, 'options.checks', 0);
423 return checks;
424}
425 
426export function checkPrimeSync(
427 candidate: PrimeNum,
428 options: CheckPrimeOptions = {}
429): boolean {
430 candidate = validateCandidate(candidate);
431 validateObject(options, 'options');
432 const checks = validateChecks(options);
433 return cryptoImpl.checkPrimeSync(candidate as ArrayBufferView, checks);
434}
435 
436export function checkPrime(
437 candidate: PrimeNum,
438 options: CheckPrimeOptions,
439 callback: CheckPrimeCallback
440): void;
441export function checkPrime(
442 candidate: PrimeNum,
443 callback: CheckPrimeCallback
444): void;
445export function checkPrime(
446 candidate: PrimeNum,
447 options: CheckPrimeOptions | CheckPrimeCallback,
448 callback?: CheckPrimeCallback
449): void {
450 candidate = validateCandidate(candidate);
451 if (typeof options === 'function') {
452 callback = options;
453 options = {};
454 }
455 validateObject(options, 'options');
456 validateFunction(callback, 'callback');
457 const checks = validateChecks(options);
458 new Promise<boolean>((res, rej) => {
459 try {
460 res(cryptoImpl.checkPrimeSync(candidate as ArrayBufferView, checks));
461 } catch (err) {
462 rej(err as Error);
463 }
464 }).then(
465 (val: boolean): void => {
466 callback(null, val);
467 },
468 (err: unknown): void => {
469 callback(err);
470 }
471 );
472}