Repository: denoland/deno
Pull request: fix(ext/node): implement safe, add, rem options for crypto.generatePrime

## Summary

- Implements the previously stubbed `safe`, `add`, and `rem` options for `crypto.generatePrime()` / `crypto.generatePrimeSync()`, matching Node.js/OpenSSL behavior
- Fixes `checkPrime` / `checkPrimeSync` bigint truncation bug where candidates > 2^63 were silently truncated to `i64`, causing incorrect primality results
- Fixes `unsignedBigIntToBuffer` using `StringPrototypeToString` (a String.prototype method) on BigInt values
- Adds `ERR_OSSL_BN_BIGNUM_TOO_LONG` validation for oversized buffer candidates
- Fixes async `generatePrime` promise error handling (`.then().catch()` → two-arg `.then(onResolve, onReject)`) to prevent double callback invocation

Changed files:
filename: ext/node/polyfills/internal/crypto/diffiehellman.ts
patch: @@ -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,
       );
     }
 

filename: ext/node/polyfills/internal/crypto/random.ts
patch: @@ -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");
 }
 

filename: ext/node_crypto/lib.rs
patch: @@ -1176,20 +1176,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<Uint8Array, GeneratePrimeError> {
+  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<primes::GeneratePrimeError> 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<JsBuffer>,
+  #[buffer] rem: Option<JsBuffer>,
+) -> Result<Uint8Array, GeneratePrimeError> {
+  gen_prime(size, safe, add.as_deref(), rem.as_deref())
 }
 
 #[op2]
 pub async fn op_node_gen_prime_async(
   #[number] size: usize,
-) -> Result<Uint8Array, tokio::task::JoinError> {
-  spawn_blocking(move || gen_prime(size)).await
+  safe: bool,
+  #[buffer] add: Option<JsBuffer>,
+  #[buffer] rem: Option<JsBuffer>,
+) -> Result<Uint8Array, GeneratePrimeError> {
+  spawn_blocking(move || gen_prime(size, safe, add.as_deref(), rem.as_deref()))
+    .await?
 }
 
 #[derive(Debug, thiserror::Error, deno_error::JsError)]

filename: ext/node_crypto/primes.rs
patch: @@ -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<Self, GeneratePrimeError> {
+    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 {

filename: tests/node_compat/config.jsonc
patch: @@ -418,6 +418,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": {},