Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
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
108 changes: 100 additions & 8 deletions src/js/node/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,20 @@ const {
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,97 @@ crypto_exports.randomBytes = randomBytes;
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 };

Comment thread
robobun marked this conversation as resolved.
// Node's argon2 path rejects KeyObject and throws node-formatted
// ERR_INVALID_ARG_TYPE, unlike the local `getArrayBufferOrView` above.
Comment thread
robobun marked this conversation as resolved.
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 a
// wrong-typed secret/associatedData names the property (node passes no name
// there and trips ERR_INTERNAL_ASSERTION on it).
Comment thread
robobun marked this conversation as resolved.
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
1 change: 0 additions & 1 deletion src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,6 @@ const errors: ErrorCodeMapping = [
["ERR_TRACE_EVENTS_UNAVAILABLE", Error],
["ERR_TRAILING_JUNK_AFTER_STREAM_END", TypeError],
["ERR_SQLITE_ERROR", Error],
["ERR_CRYPTO_ARGON2_NOT_SUPPORTED", Error],
["ERR_HTTP2_INVALID_CONNECTION_HEADERS", TypeError],
["ERR_QUIC_CONNECTION_FAILED", Error],
["ERR_QUIC_ENDPOINT_CLOSED", Error],
Expand Down
226 changes: 225 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,8 @@

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

// `rust-argon2` exports its lib as crate `argon2`; alias past the `argon2` host fn below.
use ::argon2 as rust_argon2;
use bun_boringssl as boringssl;
use bun_collections::CaseInsensitiveAsciiStringArrayHashMap;
use bun_jsc::{
Expand Down Expand Up @@ -827,6 +829,28 @@ pub(crate) struct Scrypt {
err: Option<u32>,
}

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

/// One argon2 derivation, routed to the pure-Rust `rust-argon2` crate that
/// `Bun.password` already uses (BoringSSL has no argon2). Inputs are copied
/// out of JS at call time so the work-pool half never touches JS memory;
/// node's async jobs copy the same way.
Comment thread
robobun marked this conversation as resolved.
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 +1390,191 @@ mod _impl {
Ok(buf)
}

impl Argon2 {
/// Arguments arrive pre-validated from `checkArgon2()` in `crypto.ts`;
/// the checks here only defend the internal binding itself.
Comment thread
robobun marked this conversation as resolved.
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,
));
}
};

// The validators admit sizes rust-argon2 would abort on
// (`vec![Block::zero(); mem_cost]` and the output Vec allocate
// infallibly). Pre-fail the job instead, so both paths deliver
// the same catchable error node produces when OpenSSL's argon2
// allocation fails.
Comment thread
robobun marked this conversation as resolved.
let limit = jsc::virtual_machine::synthetic_allocation_limit();
let failed =
(memory as usize).saturating_mul(1024) > limit || tag_length as usize > limit;

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,
};
Ok((ctx, callback))
}

fn run(&mut self) {
if self.failed {
return;
}
let config = rust_argon2::Config {
ad: &self.associated_data,
hash_length: self.tag_length,
lanes: self.parallelism,
mem_cost: self.memory,
secret: &self.secret,
// Sequential like Bun.password (pwhash.rs): lanes determine
// the output, not the thread count, so results match node,
// which threads lanes via OpenSSL on its worker.
Comment thread
robobun marked this conversation as resolved.
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,
// Unreachable via `node:crypto`: `checkArgon2()` bounds are a
// superset of rust-argon2's constraints.
Comment thread
robobun marked this conversation as resolved.
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 +1746,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
Loading