diff --git a/ext/node/polyfills/internal/crypto/diffiehellman.ts b/ext/node/polyfills/internal/crypto/diffiehellman.ts index 450c17056946d7..0e13870ff068f6 100644 --- a/ext/node/polyfills/internal/crypto/diffiehellman.ts +++ b/ext/node/polyfills/internal/crypto/diffiehellman.ts @@ -136,7 +136,7 @@ export class DiffieHellmanImpl { } this.#prime = Buffer.from( - op_node_gen_prime(this.#primeLength).buffer, + op_node_gen_prime(this.#primeLength, false, null, null).buffer, ); } diff --git a/ext/node/polyfills/internal/crypto/random.ts b/ext/node/polyfills/internal/crypto/random.ts index 22da15e4174b81..fdd8045f48975d 100644 --- a/ext/node/polyfills/internal/crypto/random.ts +++ b/ext/node/polyfills/internal/crypto/random.ts @@ -4,21 +4,13 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { primordials } from "ext:core/mod.js"; import { - op_node_check_prime, - op_node_check_prime_async, op_node_check_prime_bytes, op_node_check_prime_bytes_async, op_node_gen_prime, op_node_gen_prime_async, } from "ext:core/ops"; -const { - StringPrototypePadStart, - StringPrototypeToString, -} = primordials; -import { notImplemented } from "ext:deno_node/_utils.ts"; import randomBytes from "ext:deno_node/internal/crypto/_randomBytes.ts"; import randomFill, { randomFillSync, @@ -37,6 +29,8 @@ import { import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, + NodeError, + NodeRangeError, } from "ext:deno_node/internal/errors.ts"; import { Buffer } from "node:buffer"; @@ -47,6 +41,10 @@ export { } from "ext:deno_node/internal/crypto/_randomFill.mjs"; export { default as randomInt } from "ext:deno_node/internal/crypto/_randomInt.ts"; +// OpenSSL BIGNUM max size: INT_MAX / (4 * BN_BITS2) words * 8 bytes/word +// On 64-bit: (2^31 - 1) / 256 * 8 = 67108856 bytes +const OPENSSL_BIGNUM_MAX_BYTES = (((2 ** 31 - 1) / (4 * 64)) | 0) * 8; + export type LargeNumberLike = | ArrayBufferView | SharedArrayBuffer @@ -94,13 +92,24 @@ export function checkPrime( validateInt32(checks, "options.checks", 0); - let op = op_node_check_prime_bytes_async; + let candidateBytes: ArrayBufferView | ArrayBuffer; if (typeof candidate === "bigint") { if (candidate < 0) { throw new ERR_OUT_OF_RANGE("candidate", ">= 0", candidate); } - op = op_node_check_prime_async; - } else if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) { + candidateBytes = bigintToBytes(candidate); + } else if (isAnyArrayBuffer(candidate) || isArrayBufferView(candidate)) { + const byteLength = isArrayBufferView(candidate) + ? (candidate as ArrayBufferView).byteLength + : (candidate as ArrayBuffer).byteLength; + if (byteLength > OPENSSL_BIGNUM_MAX_BYTES) { + throw new NodeError( + "ERR_OSSL_BN_BIGNUM_TOO_LONG", + "bignum too long", + ); + } + candidateBytes = candidate; + } else { throw new ERR_INVALID_ARG_TYPE( "candidate", [ @@ -114,7 +123,7 @@ export function checkPrime( ); } - op(candidate, checks).then( + op_node_check_prime_bytes_async(candidateBytes, checks).then( (result) => { callback?.(null, result); }, @@ -135,9 +144,24 @@ export function checkPrimeSync( validateInt32(checks, "options.checks", 0); + let candidateBytes: ArrayBufferView | ArrayBuffer; if (typeof candidate === "bigint") { - return op_node_check_prime(candidate, checks); - } else if (!isAnyArrayBuffer(candidate) && !isArrayBufferView(candidate)) { + if (candidate < 0) { + throw new ERR_OUT_OF_RANGE("candidate", ">= 0", candidate); + } + candidateBytes = bigintToBytes(candidate); + } else if (isAnyArrayBuffer(candidate) || isArrayBufferView(candidate)) { + const byteLength = isArrayBufferView(candidate) + ? (candidate as ArrayBufferView).byteLength + : (candidate as ArrayBuffer).byteLength; + if (byteLength > OPENSSL_BIGNUM_MAX_BYTES) { + throw new NodeError( + "ERR_OSSL_BN_BIGNUM_TOO_LONG", + "bignum too long", + ); + } + candidateBytes = candidate; + } else { throw new ERR_INVALID_ARG_TYPE( "candidate", [ @@ -151,7 +175,7 @@ export function checkPrimeSync( ); } - return op_node_check_prime_bytes(candidate, checks); + return op_node_check_prime_bytes(candidateBytes, checks); } export interface GeneratePrimeOptions { @@ -177,12 +201,21 @@ export function generatePrime( validateFunction(callback, "callback"); const { bigint, + safe, + add, + rem, } = validateRandomPrimeJob(size, options); - op_node_gen_prime_async(size).then((prime: Uint8Array) => - bigint ? arrayBufferToUnsignedBigInt(prime.buffer) : prime.buffer - ).then((prime: ArrayBuffer | bigint) => { - callback?.(null, prime); - }); + op_node_gen_prime_async(size, safe, add ?? null, rem ?? null).then( + (prime: Uint8Array) => { + const result = bigint + ? arrayBufferToUnsignedBigInt(prime.buffer) + : prime.buffer; + callback?.(null, result); + }, + (err: Error) => { + callback?.(err, null as unknown as ArrayBuffer); + }, + ); } export function generatePrimeSync( @@ -191,17 +224,39 @@ export function generatePrimeSync( ): ArrayBuffer | bigint { const { bigint, + safe, + add, + rem, } = validateRandomPrimeJob(size, options); - const prime = op_node_gen_prime(size); + const prime = op_node_gen_prime(size, safe, add ?? null, rem ?? null); if (bigint) return arrayBufferToUnsignedBigInt(prime.buffer); return prime.buffer; } +interface ValidatedPrimeOptions { + safe: boolean; + bigint: boolean; + add?: Uint8Array; + rem?: Uint8Array; +} + +function toUint8Array( + value: ArrayBuffer | ArrayBufferView | Buffer, +): Uint8Array { + if (value instanceof Uint8Array) { + return value; + } + if (isArrayBufferView(value)) { + return new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + } + return new Uint8Array(value); +} + function validateRandomPrimeJob( size: number, options: GeneratePrimeOptions, -): GeneratePrimeOptions { +): ValidatedPrimeOptions { validateInt32(size, "size", 1); validateObject(options, "options"); @@ -251,19 +306,68 @@ function validateRandomPrimeJob( } } - // TODO(@littledivy): safe, add and rem options are not implemented. - if (safe || add || rem) { - notImplemented("safe, add and rem options are not implemented."); + const addBuf = add ? toUint8Array(add) : undefined; + const remBuf = rem ? toUint8Array(rem) : undefined; + + if (addBuf) { + // add must be non-zero; a zero modulus would cause division by zero in the + // Rust generator. + const addBitCount = bitCount(addBuf); + if (addBitCount === 0) { + throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.add"); + } + // Node.js/OpenSSL: bit count of add must not exceed requested size + if (addBitCount > size) { + throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.add"); + } + + if (remBuf) { + // rem must be strictly less than add + const addBigInt = bufferToBigInt(addBuf); + const remBigInt = bufferToBigInt(remBuf); + if (addBigInt <= remBigInt) { + throw new NodeRangeError("ERR_OUT_OF_RANGE", "invalid options.rem"); + } + } } return { safe, bigint, - add, - rem, + add: addBuf, + rem: remBuf, }; } +function bigintToBytes(n: bigint): Uint8Array { + if (n === 0n) return new Uint8Array([0]); + const hex = n.toString(16); + const padded = hex.length % 2 ? "0" + hex : hex; + const bytes = new Uint8Array(padded.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(padded.substring(i * 2, i * 2 + 2), 16); + } + return bytes; +} + +function bufferToBigInt(buf: Uint8Array): bigint { + let result = 0n; + for (let i = 0; i < buf.length; i++) { + result = (result << 8n) | BigInt(buf[i]); + } + return result; +} + +function bitCount(buf: Uint8Array): number { + // Count the number of significant bits in a big-endian byte array + for (let i = 0; i < buf.length; i++) { + if (buf[i] !== 0) { + return (buf.length - i) * 8 - Math.clz32(buf[i]) + 24; + } + } + return 0; +} + /** * 48 is the ASCII code for '0', 97 is the ASCII code for 'a'. * @param {number} number An integer between 0 and 15. @@ -296,8 +400,8 @@ function unsignedBigIntToBuffer(bigint: bigint, name: string) { throw new ERR_OUT_OF_RANGE(name, ">= 0", bigint); } - const hex = StringPrototypeToString(bigint, 16); - const padded = StringPrototypePadStart(hex, hex.length + (hex.length % 2), 0); + const hex = bigint.toString(16); + const padded = hex.padStart(hex.length + (hex.length % 2), "0"); return Buffer.from(padded, "hex"); } diff --git a/ext/node_crypto/lib.rs b/ext/node_crypto/lib.rs index 2855fe0857b1ff..024fd53eb39cfe 100644 --- a/ext/node_crypto/lib.rs +++ b/ext/node_crypto/lib.rs @@ -1159,20 +1159,65 @@ pub fn op_node_ecdh_compute_public_key( } #[inline] -fn gen_prime(size: usize) -> Uint8Array { - primes::Prime::generate(size).0.to_bytes_be().into() +fn gen_prime( + size: usize, + safe: bool, + add: Option<&[u8]>, + rem: Option<&[u8]>, +) -> Result { + if safe || add.is_some() || rem.is_some() { + let prime = primes::Prime::generate_with_options(size, safe, add, rem)?; + Ok(prime.0.to_bytes_be().into()) + } else { + Ok(primes::Prime::generate(size).0.to_bytes_be().into()) + } +} + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum GeneratePrimeError { + #[class("ERR_OUT_OF_RANGE")] + #[error("invalid options.add")] + InvalidAdd, + #[class("ERR_OUT_OF_RANGE")] + #[error("invalid options.rem")] + InvalidRem, + #[class("ERR_OUT_OF_RANGE")] + #[error("prime generation failed: no suitable prime found in range")] + OutOfRange, + #[class(generic)] + #[error(transparent)] + JoinError(#[from] tokio::task::JoinError), +} + +impl From for GeneratePrimeError { + fn from(e: primes::GeneratePrimeError) -> Self { + match e { + primes::GeneratePrimeError::InvalidAdd => GeneratePrimeError::InvalidAdd, + primes::GeneratePrimeError::InvalidRem => GeneratePrimeError::InvalidRem, + primes::GeneratePrimeError::OutOfRange => GeneratePrimeError::OutOfRange, + } + } } #[op2] -pub fn op_node_gen_prime(#[number] size: usize) -> Uint8Array { - gen_prime(size) +pub fn op_node_gen_prime( + #[number] size: usize, + safe: bool, + #[buffer] add: Option, + #[buffer] rem: Option, +) -> Result { + gen_prime(size, safe, add.as_deref(), rem.as_deref()) } #[op2] pub async fn op_node_gen_prime_async( #[number] size: usize, -) -> Result { - spawn_blocking(move || gen_prime(size)).await + safe: bool, + #[buffer] add: Option, + #[buffer] rem: Option, +) -> Result { + spawn_blocking(move || gen_prime(size, safe, add.as_deref(), rem.as_deref())) + .await? } #[derive(Debug, thiserror::Error, deno_error::JsError)] diff --git a/ext/node_crypto/primes.rs b/ext/node_crypto/primes.rs index 7202f4fe5532e4..5a9cee9d7fbddb 100644 --- a/ext/node_crypto/primes.rs +++ b/ext/node_crypto/primes.rs @@ -17,6 +17,200 @@ impl Prime { let mut rng = rand::thread_rng(); Self(rng.gen_prime(n)) } + + /// Generate a prime with optional constraints: + /// - `safe`: if true, generate a safe prime p where (p-1)/2 is also prime + /// - `add`/`rem`: generate a prime p such that p % add == rem + /// + /// When `add` is set but `rem` is not: + /// - If safe is false, rem defaults to 1 + /// - If safe is true, rem defaults to 3 + /// + /// This follows the same algorithm as OpenSSL's BN_generate_prime_ex(): + /// generate random candidates in [2^(n-1), 2^n), adjust to satisfy the + /// add/rem constraint, then test with Miller-Rabin. For safe primes, + /// also verify (p-1)/2 is prime. + pub fn generate_with_options( + n: usize, + safe: bool, + add: Option<&[u8]>, + rem: Option<&[u8]>, + ) -> Result { + use num_bigint_dig::BigUint; + + let add_val = add.map(BigUint::from_bytes_be); + let rem_val = rem.map(BigUint::from_bytes_be); + + if let Some(ref add_v) = add_val { + // add must be < 2^size, otherwise no prime of `size` bits can satisfy + // the constraint. + let max_val = BigUint::from(1u32) << n; + if *add_v >= max_val { + return Err(GeneratePrimeError::InvalidAdd); + } + + if let Some(ref rem_v) = rem_val { + // rem must be < add + if rem_v >= add_v { + return Err(GeneratePrimeError::InvalidRem); + } + } + + // Check that there's room for a random prime. If add >= 2^(size-1), + // and there's at most one candidate value < 2^size with the right + // residue, it's degenerate. + let min_val = BigUint::from(1u32) << (n - 1); + // Count how many candidates exist: floor((max_val - 1 - rem) / add) - floor((min_val - 1 - rem) / add) + // If there's only one (or zero), we should check if it works or error. + let effective_rem = if let Some(ref r) = rem_val { + r.clone() + } else if safe { + BigUint::from(3u32) + } else { + BigUint::from(1u32) + }; + + // If only one candidate exists and size is very small, compute it directly + if *add_v >= min_val { + // At most one or two candidates in [min_val, max_val) + // Find the candidate: smallest x >= min_val where x % add == effective_rem + let mut candidate = { + let r = &min_val % add_v; + if r <= effective_rem { + &min_val + (&effective_rem - r) + } else { + &min_val + (add_v - &r + &effective_rem) + } + }; + + // Check candidates (there should be very few with add this large) + loop { + if candidate >= max_val { + return Err(GeneratePrimeError::OutOfRange); + } + let is_prime = is_biguint_probably_prime(&candidate, 20); + let safe_ok = !safe + || is_biguint_probably_prime( + &((&candidate - BigUint::from(1u32)) >> 1), + 20, + ); + if is_prime && safe_ok { + return Ok(Self(candidate)); + } + candidate += add_v; + } + } + } + + // General case: generate random candidates + let mut rng = rand::thread_rng(); + + if !safe && add_val.is_none() { + // Simple case: just generate a random prime + return Ok(Self(rng.gen_prime(n))); + } + + let min_val = num_bigint_dig::BigUint::from(1u32) << (n - 1); + let max_val = num_bigint_dig::BigUint::from(1u32) << n; + let range = &max_val - &min_val; + + loop { + // Generate a random number in [min_val, max_val) + let random_offset = gen_biguint_below(&mut rng, &range); + let mut candidate = &min_val + random_offset; + + if let Some(ref add_v) = add_val { + let effective_rem = if let Some(ref r) = rem_val { + r.clone() + } else if safe { + num_bigint_dig::BigUint::from(3u32) + } else { + num_bigint_dig::BigUint::from(1u32) + }; + + // Adjust candidate so that candidate % add == effective_rem + let r = &candidate % add_v; + if r <= effective_rem { + candidate += &effective_rem - r; + } else { + candidate += add_v - &r + &effective_rem; + } + + // Make sure we're still in range + if candidate >= max_val { + continue; + } + if candidate < min_val { + candidate += add_v; + if candidate >= max_val { + continue; + } + } + } else { + // No add constraint, but safe is true. Make candidate odd. + candidate |= num_bigint_dig::BigUint::from(1u32); + } + + if !is_biguint_probably_prime(&candidate, 20) { + continue; + } + + if safe { + let half = (&candidate - num_bigint_dig::BigUint::from(1u32)) >> 1; + if !is_biguint_probably_prime(&half, 20) { + continue; + } + } + + return Ok(Self(candidate)); + } + } +} + +/// Generate a random BigUint in [0, bound) +fn gen_biguint_below( + rng: &mut impl Rng, + bound: &num_bigint_dig::BigUint, +) -> num_bigint_dig::BigUint { + let bits = bound.bits(); + loop { + let candidate = gen_random_biguint(rng, bits); + if candidate < *bound { + return candidate; + } + } +} + +/// Generate a random BigUint with up to `bits` bits +fn gen_random_biguint( + rng: &mut impl Rng, + bits: usize, +) -> num_bigint_dig::BigUint { + let byte_count = bits.div_ceil(8); + let mut bytes = vec![0u8; byte_count]; + rng.fill(&mut bytes[..]); + // Mask off excess bits in the top byte + if !bits.is_multiple_of(8) { + bytes[0] &= (1u8 << (bits % 8)) - 1; + } + num_bigint_dig::BigUint::from_bytes_be(&bytes) +} + +/// Check if a BigUint is probably prime using Miller-Rabin +fn is_biguint_probably_prime( + n: &num_bigint_dig::BigUint, + count: usize, +) -> bool { + // Convert to num_bigint::BigInt for our existing is_probably_prime + let bigint = BigInt::from_bytes_be(num_bigint::Sign::Plus, &n.to_bytes_be()); + is_probably_prime(&bigint, count) +} + +#[derive(Debug)] +pub enum GeneratePrimeError { + InvalidAdd, + InvalidRem, + OutOfRange, } impl From<&[u8]> for Prime { diff --git a/tests/node_compat/config.jsonc b/tests/node_compat/config.jsonc index 10e8eb18510458..c79865c60e59e2 100644 --- a/tests/node_compat/config.jsonc +++ b/tests/node_compat/config.jsonc @@ -412,6 +412,7 @@ "parallel/test-crypto-psychic-signatures.js": {}, "parallel/test-crypto-randomfillsync-regression.js": {}, "parallel/test-crypto-randomuuid.js": {}, + "parallel/test-crypto-prime.js": {}, "parallel/test-crypto-rsa-pss-default-salt-length.js": {}, "parallel/test-crypto-sec-level.js": {}, "parallel/test-crypto-secret-keygen.js": {},