From a72396fa84bde41ef5bc0734172298aa6e4d695d Mon Sep 17 00:00:00 2001 From: Levente Kurusa Date: Wed, 12 Apr 2023 02:57:57 +0200 Subject: [PATCH] refactor(node/crypto): port polyfill to Rust for randomInt, randomFill, randomFillSync (#18658) Pretty much as per the title, I'd welcome some feedback especially around the array/buffer handling in the two randomFill functions. --- .../unit_node/internal/_randomFill_test.ts | 7 +++++- ext/node/crypto/mod.rs | 12 ++++++++++ ext/node/lib.rs | 1 + .../polyfills/internal/crypto/_randomFill.ts | 24 ++++++++++++------- .../polyfills/internal/crypto/_randomInt.ts | 11 +++------ 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/cli/tests/unit_node/internal/_randomFill_test.ts b/cli/tests/unit_node/internal/_randomFill_test.ts index 00ff029cc989d3..5e2e154c8e6ff9 100644 --- a/cli/tests/unit_node/internal/_randomFill_test.ts +++ b/cli/tests/unit_node/internal/_randomFill_test.ts @@ -6,6 +6,7 @@ import { assertNotEquals, assertThrows, } from "../../../../test_util/std/testing/asserts.ts"; +import { deferred } from "../../../../test_util/std/async/deferred.ts"; const validateNonZero = (buf: Buffer) => { if (!buf.some((ch) => ch > 0)) throw new Error("Error"); @@ -15,14 +16,18 @@ const validateZero = (buf: Buffer) => { buf.forEach((val) => assertEquals(val, 0)); }; -Deno.test("[node/crypto.randomFill]", () => { +Deno.test("[node/crypto.randomFill]", async () => { + const promise = deferred(); const buf = Buffer.alloc(10); const before = buf.toString("hex"); randomFill(buf, 5, 5, (_err, bufTwo) => { const after = bufTwo?.toString("hex"); assertEquals(before.slice(0, 10), after?.slice(0, 10)); + promise.resolve(true); }); + + await promise; }); Deno.test("[node/crypto.randomFillSync]", () => { diff --git a/ext/node/crypto/mod.rs b/ext/node/crypto/mod.rs index 55a7a5870e9863..f818b96af5bbe5 100644 --- a/ext/node/crypto/mod.rs +++ b/ext/node/crypto/mod.rs @@ -9,6 +9,8 @@ use deno_core::StringOrBuffer; use deno_core::ZeroCopyBuf; use hkdf::Hkdf; use num_bigint::BigInt; +use rand::distributions::Distribution; +use rand::distributions::Uniform; use rand::Rng; use std::future::Future; use std::rc::Rc; @@ -478,3 +480,13 @@ pub async fn op_node_hkdf_async( }) .await? } + +#[op] +pub fn op_node_random_int(min: i32, max: i32) -> Result { + let mut rng = rand::thread_rng(); + // Uniform distribution is required to avoid Modulo Bias + // https://en.wikipedia.org/wiki/Fisher–Yates_shuffle#Modulo_bias + let dist = Uniform::from(min..max); + + Ok(dist.sample(&mut rng)) +} diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 3ef761cb7c3345..b73c3366ff48e0 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -194,6 +194,7 @@ deno_core::extension!(deno_node, crypto::op_node_generate_secret, crypto::op_node_generate_secret_async, crypto::op_node_sign, + crypto::op_node_random_int, crypto::x509::op_node_x509_parse, crypto::x509::op_node_x509_ca, crypto::x509::op_node_x509_check_email, diff --git a/ext/node/polyfills/internal/crypto/_randomFill.ts b/ext/node/polyfills/internal/crypto/_randomFill.ts index ba8b9cbec14cf2..89c374c0251662 100644 --- a/ext/node/polyfills/internal/crypto/_randomFill.ts +++ b/ext/node/polyfills/internal/crypto/_randomFill.ts @@ -1,8 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -import randomBytes, { +import { MAX_SIZE as kMaxUint32, } from "ext:deno_node/internal/crypto/_randomBytes.ts"; import { Buffer } from "ext:deno_node/buffer.ts"; +const { core } = globalThis.__bootstrap; +const { ops } = core; const kBufferMaxLength = 0x7fffffff; @@ -62,11 +64,14 @@ export default function randomFill( assertOffset(offset as number, buf.length); assertSize(size as number, offset as number, buf.length); - randomBytes(size as number, (err, bytes) => { - if (err) return cb!(err, buf); - bytes?.copy(buf, offset as number); - cb!(null, buf); - }); + core.opAsync("op_node_generate_secret_async", Math.floor(size as number)) + .then( + (randomData: Uint8Array) => { + const randomBuf = Buffer.from(randomData.buffer); + randomBuf.copy(buf, offset as number, 0, size as number); + cb!(null, buf); + }, + ); } export function randomFillSync(buf: Buffer, offset = 0, size?: number) { @@ -76,9 +81,10 @@ export function randomFillSync(buf: Buffer, offset = 0, size?: number) { assertSize(size, offset, buf.length); - const bytes = randomBytes(size); - - bytes.copy(buf, offset); + const bytes: Uint8Array = new Uint8Array(Math.floor(size)); + ops.op_node_generate_secret(bytes); + const bytesBuf: Buffer = Buffer.from(bytes.buffer); + bytesBuf.copy(buf, offset, 0, size); return buf; } diff --git a/ext/node/polyfills/internal/crypto/_randomInt.ts b/ext/node/polyfills/internal/crypto/_randomInt.ts index 6372515410f144..4e55eba9de2e18 100644 --- a/ext/node/polyfills/internal/crypto/_randomInt.ts +++ b/ext/node/polyfills/internal/crypto/_randomInt.ts @@ -1,4 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +const ops = globalThis.Deno.core.ops; + export default function randomInt(max: number): number; export default function randomInt(min: number, max: number): number; export default function randomInt( @@ -40,16 +42,9 @@ export default function randomInt( throw new Error("Min is bigger than Max!"); } - const randomBuffer = new Uint32Array(1); - - globalThis.crypto.getRandomValues(randomBuffer); - - const randomNumber = randomBuffer[0] / (0xffffffff + 1); - min = Math.ceil(min); max = Math.floor(max); - - const result = Math.floor(randomNumber * (max - min)) + min; + const result = ops.op_node_random_int(min, max); if (cb) { cb(null, result);