Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 102 additions & 8 deletions src/js/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,20 @@
getHashes,
scrypt,
scryptSync,
argon2: _argon2,
argon2Sync: _argon2Sync,
} = $rust("node_crypto_binding.rs", "createNodeCryptoBindingZig");

const normalizeEncoding = $newRustFunction("node_util_binding.rs", "normalizeEncoding", 1);

const { validateString } = require("internal/validators");
const {
validateFunction,
validateInteger,
validateObject,
validateOneOf,
validateString,
validateUint32,
} = require("internal/validators");
const { deprecate } = require("internal/util/deprecate");

const kHandle = Symbol("kHandle");
Expand Down Expand Up @@ -334,14 +343,99 @@
crypto_exports.randomUUID = randomUUID;
crypto_exports.randomUUIDv7 = randomUUIDv7;

// Node only provides Argon2 when built against OpenSSL >= 3.2; BoringSSL has no
// Argon2, so these throw the same error an unsupported Node build throws.
// The unused parameters are declared to keep `.length` equal to Node's (3 and 2).
crypto_exports.argon2 = function argon2(_algorithm, _parameters, _callback) {
throw $ERR_CRYPTO_ARGON2_NOT_SUPPORTED("Argon2 algorithm not supported");
const kArgon2Types = { __proto__: null, argon2d: 0, argon2i: 1, argon2id: 2 };

Check warning on line 347 in src/js/node/crypto.ts

View check run for this annotation

Claude / Claude Code Review

Dead error code ERR_CRYPTO_ARGON2_NOT_SUPPORTED left in ErrorCode.ts

This PR removes the last two callers of `$ERR_CRYPTO_ARGON2_NOT_SUPPORTED` (and the test that asserted it), so the registration entry `["ERR_CRYPTO_ARGON2_NOT_SUPPORTED", Error]` at `src/jsc/bindings/ErrorCode.ts:359` is now dead. Per REVIEW.md ("Delete dead code in the same PR that makes it dead"), that ErrorCode.ts line should be dropped in this PR.
Comment thread
robobun marked this conversation as resolved.
// Node's argon2 path (getArrayBufferOrView in lib/internal/crypto/util.js)
// does not accept KeyObject and throws node-formatted ERR_INVALID_ARG_TYPE,
// unlike the local `getArrayBufferOrView` above.
Comment thread
robobun marked this conversation as resolved.
Outdated
function getArgon2BufferSource(buffer, name) {
if (isAnyArrayBuffer(buffer)) return buffer;
if (typeof buffer === "string") return Buffer.from(buffer, "utf8");
if (!isArrayBufferView(buffer)) {
throw $ERR_INVALID_ARG_TYPE(name, ["string", "ArrayBuffer", "Buffer", "TypedArray", "DataView"], buffer);
}
return buffer;
}

// Mirrors `check()` in node's lib/internal/crypto/argon2.js, except that a
// wrong-typed secret/associatedData names the offending property: node passes
// no name there and its error constructor asserts on the undefined name
// (ERR_INTERNAL_ASSERTION).
Comment thread
robobun marked this conversation as resolved.
Outdated
function checkArgon2(algorithm, parameters) {
validateString(algorithm, "algorithm");
validateOneOf(algorithm, "algorithm", ["argon2d", "argon2i", "argon2id"]);
const type = kArgon2Types[algorithm];

validateObject(parameters, "parameters");

const { parallelism, tagLength, memory, passes } = parameters;
const MAX_POSITIVE_UINT_32 = 2 ** 32 - 1;

const message = getArgon2BufferSource(parameters.message, "parameters.message");
validateInteger(message.byteLength, "parameters.message.byteLength", 0, MAX_POSITIVE_UINT_32);

const nonce = getArgon2BufferSource(parameters.nonce, "parameters.nonce");
validateInteger(nonce.byteLength, "parameters.nonce.byteLength", 8, MAX_POSITIVE_UINT_32);

validateInteger(parallelism, "parameters.parallelism", 1, 2 ** 24 - 1);
validateInteger(tagLength, "parameters.tagLength", 4, MAX_POSITIVE_UINT_32);
validateInteger(memory, "parameters.memory", 8 * parallelism, MAX_POSITIVE_UINT_32);
validateUint32(passes, "parameters.passes", true);

let secret = parameters.secret;
if (secret === undefined) {
secret = new Uint8Array(0);
} else {
secret = getArgon2BufferSource(secret, "parameters.secret");
validateInteger(secret.byteLength, "parameters.secret.byteLength", 0, MAX_POSITIVE_UINT_32);
}

let associatedData = parameters.associatedData;
if (associatedData === undefined) {
associatedData = new Uint8Array(0);
} else {
associatedData = getArgon2BufferSource(associatedData, "parameters.associatedData");
validateInteger(associatedData.byteLength, "parameters.associatedData.byteLength", 0, MAX_POSITIVE_UINT_32);
}

return { message, nonce, secret, associatedData, tagLength, passes, parallelism, memory, type };
}

crypto_exports.argon2 = function argon2(algorithm, parameters, callback) {
parameters = checkArgon2(algorithm, parameters);

validateFunction(callback, "callback");

_argon2(
parameters.message,
parameters.nonce,
parameters.parallelism,
parameters.tagLength,
parameters.memory,
parameters.passes,
parameters.secret,
parameters.associatedData,
parameters.type,
(err, result) => {
if (err !== undefined) return callback(err);
callback(null, result);
},
);
};
crypto_exports.argon2Sync = function argon2Sync(_algorithm, _parameters) {
throw $ERR_CRYPTO_ARGON2_NOT_SUPPORTED("Argon2 algorithm not supported");
crypto_exports.argon2Sync = function argon2Sync(algorithm, parameters) {
parameters = checkArgon2(algorithm, parameters);

return _argon2Sync(
parameters.message,
parameters.nonce,
parameters.parallelism,
parameters.tagLength,
parameters.memory,
parameters.passes,
parameters.secret,
parameters.associatedData,
parameters.type,
);
};

crypto_exports.checkPrime = checkPrime;
Expand Down
220 changes: 219 additions & 1 deletion src/runtime/node/node_crypto_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

use core::ffi::{c_char, c_void};

// The `rust-argon2` package exports its lib as crate name `argon2`; alias it
// so it can't be confused with the `argon2` host fn below.
Comment thread
robobun marked this conversation as resolved.
Outdated
use ::argon2 as rust_argon2;
use bun_boringssl as boringssl;
use bun_collections::CaseInsensitiveAsciiStringArrayHashMap;
use bun_jsc::{
Expand Down Expand Up @@ -827,6 +830,30 @@ pub(crate) struct Scrypt {
err: Option<u32>,
}

// ───────────────────────────────────────────────────────────────────────────
// Argon2 (crypto.argon2 / crypto.argon2Sync)
// ───────────────────────────────────────────────────────────────────────────
Comment thread
robobun marked this conversation as resolved.

/// One argon2 derivation. BoringSSL has no argon2, so this routes to the
/// pure-Rust `rust-argon2` crate `Bun.password` already uses (pwhash.rs).
///
/// All inputs are copied out of JS at call time, so the work-pool half never
/// touches JS memory (no protect/detach hazards); node copies async-job
/// inputs the same way (`ToCopy` in crypto_argon2.cc).
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) struct Argon2 {
message: Vec<u8>,
nonce: Vec<u8>,
secret: Vec<u8>,
associated_data: Vec<u8>,
parallelism: u32,
tag_length: u32,
memory: u32,
passes: u32,
variant: rust_argon2::Variant,
output: Vec<u8>,
failed: bool,
}

mod _impl {
use super::*;
use crate::node::util::validators;
Expand Down Expand Up @@ -1366,8 +1393,182 @@ mod _impl {
Ok(buf)
}

impl Argon2 {
/// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`
/// (mirroring node's `lib/internal/crypto/argon2.js`); the checks here
/// only defend the internal binding itself.
Comment thread
robobun marked this conversation as resolved.
Outdated
fn from_js(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult<(Self, JSValue)> {
fn copy_buffer_arg(
global: &JSGlobalObject,
value: JSValue,
name: &'static [u8],
) -> JsResult<Vec<u8>> {
let Some(buf) = value.as_array_buffer(global) else {
return Err(global.throw_invalid_argument_type_value(
name,
b"ArrayBuffer, Buffer, TypedArray, or DataView",
value,
));
};
let bytes = buf.byte_slice();
let mut copy = Vec::new();
if copy.try_reserve_exact(bytes.len()).is_err() {
return Err(global.throw_out_of_memory());
}
copy.extend_from_slice(bytes);
Ok(copy)
}

let [
message_value,
nonce_value,
parallelism_value,
tag_length_value,
memory_value,
passes_value,
secret_value,
associated_data_value,
variant_value,
callback,
] = call_frame.arguments_as_array::<10>();

let parallelism = validators::validate_uint32(
global,
parallelism_value,
format_args!("parameters.parallelism"),
true,
)?;
let tag_length = validators::validate_uint32(
global,
tag_length_value,
format_args!("parameters.tagLength"),
true,
)?;
let memory = validators::validate_uint32(
global,
memory_value,
format_args!("parameters.memory"),
true,
)?;
let passes = validators::validate_uint32(
global,
passes_value,
format_args!("parameters.passes"),
true,
)?;
let variant = match validators::validate_uint32(
global,
variant_value,
format_args!("type"),
false,
)? {
0 => rust_argon2::Variant::Argon2d,
1 => rust_argon2::Variant::Argon2i,
2 => rust_argon2::Variant::Argon2id,
_ => {
return Err(global.throw_invalid_argument_type_value(
b"type",
b"a supported argon2 type",
variant_value,
));
}
};

let ctx = Argon2 {
message: copy_buffer_arg(global, message_value, b"message")?,
nonce: copy_buffer_arg(global, nonce_value, b"nonce")?,
secret: copy_buffer_arg(global, secret_value, b"secret")?,
associated_data: copy_buffer_arg(global, associated_data_value, b"associatedData")?,
parallelism,
tag_length,
memory,
passes,
variant,
output: Vec::new(),
failed: false,
};
Ok((ctx, callback))
}

fn run(&mut self) {
let config = rust_argon2::Config {
ad: &self.associated_data,
hash_length: self.tag_length,
lanes: self.parallelism,
mem_cost: self.memory,
secret: &self.secret,
// Lanes shape the memory layout (and the output) but are
// computed on the calling thread; never spawn threads from a
// work-pool worker. Matches pwhash.rs.
Comment thread
robobun marked this conversation as resolved.
Outdated
thread_mode: rust_argon2::ThreadMode::Sequential,
time_cost: self.passes,
variant: self.variant,
version: rust_argon2::Version::Version13,
};
match rust_argon2::hash_raw(&self.message, &self.nonce, &config) {
Ok(hash) => self.output = hash,
// `checkArgon2()` enforces node's parameter ranges, which are a
// superset of rust-argon2's own constraints, so this is
// unreachable through `node:crypto`; keep node's error message
// for the internal binding.
Comment thread
robobun marked this conversation as resolved.
Outdated
Err(_) => self.failed = true,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

impl CryptoJobCtx for Argon2 {
fn init(&mut self, _global: &JSGlobalObject) -> JsResult<()> {
Ok(())
}

fn run_task(&mut self) {
self.run();
}

fn run_from_js(&mut self, global: &JSGlobalObject, callback: JSValue) {
let event_loop = global.bun_vm().event_loop_mut();
if self.failed {
let exception =
global.create_error_instance(format_args!("Argon2 derivation failed"));
event_loop.run_callback(callback, global, JSValue::UNDEFINED, &[exception]);
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let output = core::mem::take(&mut self.output);
// Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator).
let buf = JSValue::create_buffer(global, output.leak());
Comment thread
robobun marked this conversation as resolved.
Outdated
event_loop.run_callback(
callback,
global,
JSValue::UNDEFINED,
&[JSValue::UNDEFINED, buf],
);
}

fn deinit(&mut self) {}
}

#[bun_jsc::host_fn]
fn argon2(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult<JSValue> {
let (ctx, callback) = Argon2::from_js(global, call_frame)?;
let _ = validators::validate_function(global, "callback", callback)?;
crypto_job_init_and_schedule(global, callback, ctx)?;
Ok(JSValue::UNDEFINED)
}

#[bun_jsc::host_fn]
fn argon2_sync(global: &JSGlobalObject, call_frame: &CallFrame) -> JsResult<JSValue> {
let (mut ctx, _) = Argon2::from_js(global, call_frame)?;
ctx.run();
if ctx.failed {
let err = global.create_error_instance(format_args!("Argon2 derivation failed"));
return Err(global.throw_value(err));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Ownership transfers to JSC (freed via MarkedArrayBuffer_deallocator).
Ok(JSValue::create_buffer(global, ctx.output.leak()))
}

pub(crate) fn create_node_crypto_binding_zig(global: &JSGlobalObject) -> JSValue {
let crypto = JSValue::create_empty_object(global, 15);
let crypto = JSValue::create_empty_object(global, 17);

// `#[bun_jsc::host_fn]` emits a `__jsc_host_{name}` shim with the raw `JSHostFn` ABI;
// pass that (not the safe-Rust body) to `JSFunction::create`.
Expand Down Expand Up @@ -1539,6 +1740,23 @@ mod _impl {
),
);

crypto.put(
global,
b"argon2",
JSFunction::create(global, "argon2", __jsc_host_argon2, 10, Default::default()),
);
crypto.put(
global,
b"argon2Sync",
JSFunction::create(
global,
"argon2Sync",
__jsc_host_argon2_sync,
9,
Default::default(),
),
);

crypto
}
} // mod _impl
Expand Down
Loading