diff --git a/bench/crypto/diffie-hellman.mjs b/bench/crypto/diffie-hellman.mjs new file mode 100644 index 0000000000..f9451b8658 --- /dev/null +++ b/bench/crypto/diffie-hellman.mjs @@ -0,0 +1,53 @@ +import crypto from "node:crypto"; +import { bench, run } from "../runner.mjs"; + +// Pre-generate DH params to avoid including setup in benchmarks +const dhSize = 1024; // Reduced from 2048 for faster testing +const dh = crypto.createDiffieHellman(dhSize); +const dhPrime = dh.getPrime(); +const dhGenerator = dh.getGenerator(); + +// Classical Diffie-Hellman +bench("DH - generateKeys", () => { + const alice = crypto.createDiffieHellman(dhPrime, dhGenerator); + return alice.generateKeys(); +}); + +bench("DH - computeSecret", () => { + // Setup + const alice = crypto.createDiffieHellman(dhPrime, dhGenerator); + const aliceKey = alice.generateKeys(); + const bob = crypto.createDiffieHellman(dhPrime, dhGenerator); + const bobKey = bob.generateKeys(); + + // Benchmark just the secret computation + return alice.computeSecret(bobKey); +}); + +// ECDH with prime256v1 (P-256) +bench("ECDH-P256 - generateKeys", () => { + const ecdh = crypto.createECDH("prime256v1"); + return ecdh.generateKeys(); +}); + +bench("ECDH-P256 - computeSecret", () => { + // Setup + const alice = crypto.createECDH("prime256v1"); + const aliceKey = alice.generateKeys(); + const bob = crypto.createECDH("prime256v1"); + const bobKey = bob.generateKeys(); + + // Benchmark just the secret computation + return alice.computeSecret(bobKey); +}); + +// ECDH with secp384r1 (P-384) +bench("ECDH-P384 - computeSecret", () => { + const alice = crypto.createECDH("secp384r1"); + const aliceKey = alice.generateKeys(); + const bob = crypto.createECDH("secp384r1"); + const bobKey = bob.generateKeys(); + return alice.computeSecret(bobKey); +}); + +await run(); diff --git a/bench/crypto/ecdh-convert-key.mjs b/bench/crypto/ecdh-convert-key.mjs new file mode 100644 index 0000000000..43049128b3 --- /dev/null +++ b/bench/crypto/ecdh-convert-key.mjs @@ -0,0 +1,44 @@ +import crypto from "node:crypto"; +import { bench, run } from "../runner.mjs"; + +function generateTestKeyPairs() { + const curves = crypto.getCurves(); + const keys = {}; + + for (const curve of curves) { + const ecdh = crypto.createECDH(curve); + ecdh.generateKeys(); + + keys[curve] = { + compressed: ecdh.getPublicKey("hex", "compressed"), + uncompressed: ecdh.getPublicKey("hex", "uncompressed"), + instance: ecdh, + }; + } + + return keys; +} + +const testKeys = generateTestKeyPairs(); + +bench("ECDH key format - P256 compressed to uncompressed", () => { + const publicKey = testKeys["prime256v1"].compressed; + return crypto.ECDH.convertKey(publicKey, "prime256v1", "hex", "hex", "uncompressed"); +}); + +bench("ECDH key format - P256 uncompressed to compressed", () => { + const publicKey = testKeys["prime256v1"].uncompressed; + return crypto.ECDH.convertKey(publicKey, "prime256v1", "hex", "hex", "compressed"); +}); + +bench("ECDH key format - P384 compressed to uncompressed", () => { + const publicKey = testKeys["secp384r1"].compressed; + return crypto.ECDH.convertKey(publicKey, "secp384r1", "hex", "hex", "uncompressed"); +}); + +bench("ECDH key format - P384 uncompressed to compressed", () => { + const publicKey = testKeys["secp384r1"].uncompressed; + return crypto.ECDH.convertKey(publicKey, "secp384r1", "hex", "hex", "compressed"); +}); + +await run(); diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 965ac51adb..23af270a91 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -1365,14 +1365,14 @@ JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue w // throw warning; // }); auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_throwValue, JSC::ImplementationVisibility::Private); - process->queueNextTick(vm, globalObject, func, errorInstance); + process->queueNextTick(globalObject, func, errorInstance); return jsUndefined(); } } // process.nextTick(doEmitWarning, warning); auto func = JSFunction::create(vm, globalObject, 1, ""_s, jsFunction_emitWarning, JSC::ImplementationVisibility::Private); - process->queueNextTick(vm, globalObject, func, errorInstance); + process->queueNextTick(globalObject, func, errorInstance); return jsUndefined(); } @@ -2272,7 +2272,7 @@ JSC_DEFINE_HOST_FUNCTION(Bun__Process__disconnect, (JSGlobalObject * globalObjec auto finishFn = JSC::JSFunction::create(vm, globalObject, 0, String("finish"_s), processDisonnectFinish, ImplementationVisibility::Public); auto process = jsCast(global->processObject()); - process->queueNextTick(vm, globalObject, finishFn); + process->queueNextTick(globalObject, finishFn); return JSC::JSValue::encode(jsUndefined()); } @@ -3096,8 +3096,9 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionDrainMicrotaskQueue, (JSC::JSGlobalObject * g return JSValue::encode(jsUndefined()); } -void Process::queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, const ArgList& args) +void Process::queueNextTick(JSC::JSGlobalObject* globalObject, const ArgList& args) { + auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); if (!this->m_nextTickFunction) { this->get(globalObject, Identifier::fromString(vm, "nextTick"_s)); @@ -3110,16 +3111,16 @@ void Process::queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, cons RELEASE_AND_RETURN(scope, void()); } -void Process::queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue value) +void Process::queueNextTick(JSC::JSGlobalObject* globalObject, JSValue value) { ASSERT_WITH_MESSAGE(value.isCallable(), "Must be a function for us to call"); MarkedArgumentBuffer args; if (value != 0) args.append(value); - this->queueNextTick(vm, globalObject, args); + this->queueNextTick(globalObject, args); } -void Process::queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue value, JSValue arg1) +void Process::queueNextTick(JSC::JSGlobalObject* globalObject, JSValue value, JSValue arg1) { ASSERT_WITH_MESSAGE(value.isCallable(), "Must be a function for us to call"); MarkedArgumentBuffer args; @@ -3129,16 +3130,34 @@ void Process::queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSVa args.append(arg1); } } - this->queueNextTick(vm, globalObject, args); + this->queueNextTick(globalObject, args); } -extern "C" void Bun__Process__queueNextTick1(GlobalObject* globalObject, EncodedJSValue value, EncodedJSValue arg1) +template +void Process::queueNextTick(JSC::JSGlobalObject* globalObject, JSValue func, const JSValue (&args)[NumArgs]) +{ + ASSERT_WITH_MESSAGE(func.isCallable(), "Must be a function for us to call"); + MarkedArgumentBuffer argsBuffer; + argsBuffer.ensureCapacity(NumArgs + 1); + if (func != 0) { + argsBuffer.append(func); + for (size_t i = 0; i < NumArgs; i++) { + argsBuffer.append(args[i]); + } + } + this->queueNextTick(globalObject, argsBuffer); +} + +extern "C" void Bun__Process__queueNextTick1(GlobalObject* globalObject, EncodedJSValue func, EncodedJSValue arg1) { auto process = jsCast(globalObject->processObject()); - auto& vm = JSC::getVM(globalObject); - process->queueNextTick(vm, globalObject, JSValue::decode(value), JSValue::decode(arg1)); + process->queueNextTick(globalObject, JSValue::decode(func), JSValue::decode(arg1)); +} +extern "C" void Bun__Process__queueNextTick2(GlobalObject* globalObject, EncodedJSValue func, EncodedJSValue arg1, EncodedJSValue arg2) +{ + auto process = jsCast(globalObject->processObject()); + process->queueNextTick<2>(globalObject, JSValue::decode(func), { JSValue::decode(arg1), JSValue::decode(arg2) }); } -JSC_DECLARE_HOST_FUNCTION(Bun__Process__queueNextTick1); JSValue Process::constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObject) { diff --git a/src/bun.js/bindings/BunProcess.h b/src/bun.js/bindings/BunProcess.h index b8abdc566d..b9f7bd8265 100644 --- a/src/bun.js/bindings/BunProcess.h +++ b/src/bun.js/bindings/BunProcess.h @@ -49,9 +49,12 @@ public: static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable; JSValue constructNextTickFn(JSC::VM& vm, Zig::GlobalObject* globalObject); - void queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, const ArgList& args); - void queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue); - void queueNextTick(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSValue, JSValue); + void queueNextTick(JSC::JSGlobalObject* globalObject, const ArgList& args); + void queueNextTick(JSC::JSGlobalObject* globalObject, JSValue); + void queueNextTick(JSC::JSGlobalObject* globalObject, JSValue, JSValue); + + template + void queueNextTick(JSC::JSGlobalObject* globalObject, JSValue func, const JSValue (&args)[NumArgs]); static JSValue emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue warning, JSValue type, JSValue code, JSValue ctor); diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 47ae7817d6..167c22e20f 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -934,6 +934,18 @@ JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalO return {}; } +JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& scope, JSGlobalObject* globalObject, JSValue encodingValue) +{ + WTF::String encodingString = encodingValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + WTF::StringBuilder builder; + builder.append("Unknown encoding: "_s); + builder.append(encodingString); + scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_ENCODING, builder.toString())); + return {}; +} + JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg) { auto message = makeString("Invalid state: "_s, statemsg); @@ -1016,6 +1028,19 @@ JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlo return {}; } +JSC::EncodedJSValue CRYPTO_INVALID_KEYTYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message) +{ + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, message)); + return {}; +} + +JSC::EncodedJSValue CRYPTO_INVALID_KEYTYPE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject) +{ + auto message = "Invalid key type"_s; + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, message)); + return {}; +} + JSC::EncodedJSValue CRYPTO_OPERATION_FAILED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral message) { throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, message)); @@ -1036,6 +1061,15 @@ JSC::EncodedJSValue CRYPTO_ECDH_INVALID_PUBLIC_KEY(JSC::ThrowScope& throwScope, return {}; } +JSC::EncodedJSValue CRYPTO_ECDH_INVALID_FORMAT(ThrowScope& scope, JSGlobalObject* globalObject, const WTF::String& formatString) +{ + WTF::StringBuilder builder; + builder.append("Invalid ECDH format: "_s); + builder.append(formatString); + scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_ECDH_INVALID_FORMAT, builder.toString())); + return {}; +} + JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& curve) { WTF::StringBuilder builder; diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 46078b5a99..44e097d955 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -80,6 +80,7 @@ JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JS JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding); +JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope&, JSC::JSGlobalObject*, JSValue encodingValue); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); JSC::EncodedJSValue STRING_TOO_LONG(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue BUFFER_OUT_OF_BOUNDS(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral name); @@ -92,6 +93,7 @@ JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlo JSC::EncodedJSValue CRYPTO_OPERATION_FAILED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, ASCIILiteral message); JSC::EncodedJSValue CRYPTO_INVALID_KEYPAIR(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue CRYPTO_ECDH_INVALID_PUBLIC_KEY(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); +JSC::EncodedJSValue CRYPTO_ECDH_INVALID_FORMAT(JSC::ThrowScope&, JSC::JSGlobalObject*, const WTF::String& formatString); JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope&, JSC::JSGlobalObject*, const WTF::String&); JSC::EncodedJSValue CRYPTO_INVALID_JWK(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue CRYPTO_SIGN_KEY_REQUIRED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); @@ -103,6 +105,8 @@ JSC::EncodedJSValue CRYPTO_HASH_UPDATE_FAILED(JSC::ThrowScope& throwScope, JSC:: JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message); JSC::EncodedJSValue CRYPTO_TIMING_SAFE_EQUAL_LENGTH(JSC::ThrowScope&, JSC::JSGlobalObject*); JSC::EncodedJSValue CRYPTO_UNKNOWN_DH_GROUP(JSC::ThrowScope&, JSC::JSGlobalObject*); +JSC::EncodedJSValue CRYPTO_INVALID_KEYTYPE(JSC::ThrowScope&, JSC::JSGlobalObject*, WTF::ASCIILiteral message); +JSC::EncodedJSValue CRYPTO_INVALID_KEYTYPE(JSC::ThrowScope&, JSC::JSGlobalObject*); // URL diff --git a/src/bun.js/bindings/JSValue.zig b/src/bun.js/bindings/JSValue.zig index 06c2b7e79b..32a31f80cc 100644 --- a/src/bun.js/bindings/JSValue.zig +++ b/src/bun.js/bindings/JSValue.zig @@ -625,6 +625,18 @@ pub const JSValue = enum(i64) { ).unwrap(); } + pub fn callNextTick(function: JSValue, global: *JSGlobalObject, args: anytype) void { + if (Environment.isDebug) { + bun.assert(function.isCallable(global.vm())); + } + const num_args = @typeInfo(@TypeOf(args)).array.len; + switch (num_args) { + 1 => JSC.Bun__Process__queueNextTick1(@ptrCast(global), function, args[0]), + 2 => JSC.Bun__Process__queueNextTick2(@ptrCast(global), function, args[0], args[1]), + else => @compileError("needs more copy paste"), + } + } + /// The value cannot be empty. Check `!this.isEmpty()` before calling this function pub fn jsType( this: JSValue, @@ -2699,3 +2711,4 @@ const JSHostFunctionType = JSC.JSHostFunctionType; extern "c" fn AsyncContextFrame__withAsyncContextIfNeeded(global: *JSGlobalObject, callback: JSValue) JSValue; extern "c" fn Bun__JSValue__isAsyncContextFrame(value: JSValue) bool; const FetchHeaders = JSC.FetchHeaders; +const Environment = bun.Environment; diff --git a/src/bun.js/bindings/node/crypto/CryptoUtil.cpp b/src/bun.js/bindings/node/crypto/CryptoUtil.cpp index dfeb4622ae..0c50cf8d9d 100644 --- a/src/bun.js/bindings/node/crypto/CryptoUtil.cpp +++ b/src/bun.js/bindings/node/crypto/CryptoUtil.cpp @@ -441,7 +441,7 @@ JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, Throw RETURN_IF_EXCEPTION(scope, {}); if (!maybeEncoding && !defaultBufferEncoding) { - throwError(globalObject, scope, Bun::ErrorCode::ERR_UNKNOWN_ENCODING, "Invalid encoding"_s); + ERR::UNKNOWN_ENCODING(scope, globalObject, encodingValue); return {}; } diff --git a/src/bun.js/bindings/node/crypto/JSECDH.cpp b/src/bun.js/bindings/node/crypto/JSECDH.cpp index faaa71b6b8..191ca0947a 100644 --- a/src/bun.js/bindings/node/crypto/JSECDH.cpp +++ b/src/bun.js/bindings/node/crypto/JSECDH.cpp @@ -9,6 +9,8 @@ #include #include #include +#include "BufferEncodingType.h" +#include "CryptoUtil.h" namespace Bun { @@ -27,6 +29,71 @@ void JSECDH::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(thisObject, visitor); } +point_conversion_form_t JSECDH::getFormat(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue formatValue) +{ + if (formatValue.pureToBoolean() != TriState::False) { + WTF::String formatString = formatValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + if (formatString == "compressed"_s) { + return POINT_CONVERSION_COMPRESSED; + } + + if (formatString == "hybrid"_s) { + return POINT_CONVERSION_HYBRID; + } + + if (formatString != "uncompressed"_s) { + Bun::ERR::CRYPTO_ECDH_INVALID_FORMAT(scope, globalObject, formatString); + } + } + return POINT_CONVERSION_UNCOMPRESSED; +} + +EncodedJSValue JSECDH::getPublicKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue encodingValue, JSValue formatValue) +{ + point_conversion_form_t form = JSECDH::getFormat(globalObject, scope, formatValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Get the group and public key + const auto group = m_key.getGroup(); + const auto pubKey = m_key.getPublicKey(); + if (!pubKey) { + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to get ECDH public key"_s); + return {}; + } + + // Calculate the length needed for the result + size_t bufLen = EC_POINT_point2oct(group, pubKey, form, nullptr, 0, nullptr); + if (bufLen == 0) { + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to determine size for public key encoding"_s); + return {}; + } + + // Create a buffer to hold the result + auto result = JSC::ArrayBuffer::tryCreate(bufLen, 1); + if (!result) { + throwError(globalObject, scope, ErrorCode::ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate buffer for public key"_s); + return {}; + } + + // Encode the point to the buffer + if (EC_POINT_point2oct(group, pubKey, form, static_cast(result->data()), bufLen, nullptr) == 0) { + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to encode public key"_s); + return {}; + } + + // Handle output encoding if provided + BufferEncodingType encodingType = getEncodingDefaultBuffer(globalObject, scope, encodingValue); + RETURN_IF_EXCEPTION(scope, {}); + + // Create a span from the result data for encoding + std::span resultSpan(static_cast(result->data()), bufLen); + + // Return the encoded result + return StringBytes::encode(globalObject, scope, resultSpan, encodingType); +} + DEFINE_VISIT_CHILDREN(JSECDH); void setupECDHClassStructure(JSC::LazyClassStructure::Initializer& init) diff --git a/src/bun.js/bindings/node/crypto/JSECDH.h b/src/bun.js/bindings/node/crypto/JSECDH.h index d1eaf78ef8..ea0907e37e 100644 --- a/src/bun.js/bindings/node/crypto/JSECDH.h +++ b/src/bun.js/bindings/node/crypto/JSECDH.h @@ -23,9 +23,9 @@ public: return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - static JSECDH* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject, ncrypto::ECKeyPointer&& key) + static JSECDH* create(JSC::VM& vm, JSC::Structure* structure, JSC::JSGlobalObject* globalObject, ncrypto::ECKeyPointer&& key, const EC_GROUP* group) { - JSECDH* instance = new (NotNull, JSC::allocateCell(vm)) JSECDH(vm, structure, WTFMove(key)); + JSECDH* instance = new (NotNull, JSC::allocateCell(vm)) JSECDH(vm, structure, WTFMove(key), group); instance->finishCreation(vm, globalObject); return instance; } @@ -43,20 +43,24 @@ public: [](auto& spaces, auto&& space) { spaces.m_subspaceForJSECDH = std::forward(space); }); } - const ncrypto::ECKeyPointer& key() const { return m_key; } - void setKey(ncrypto::ECKeyPointer&& key) { m_key = WTFMove(key); } + ncrypto::ECKeyPointer m_key; + const EC_GROUP* m_group; + + JSC::EncodedJSValue getPublicKey(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue encodingValue, JSC::JSValue formatValue); + + static point_conversion_form_t getFormat(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue formatValue); private: - JSECDH(JSC::VM& vm, JSC::Structure* structure, ncrypto::ECKeyPointer&& key) + JSECDH(JSC::VM& vm, JSC::Structure* structure, ncrypto::ECKeyPointer&& key, const EC_GROUP* group) : Base(vm, structure) , m_key(WTFMove(key)) + , m_group(group) { + ASSERT(m_group); } void finishCreation(JSC::VM&, JSC::JSGlobalObject*); static void destroy(JSC::JSCell* cell) { static_cast(cell)->~JSECDH(); } - - ncrypto::ECKeyPointer m_key; }; void setupECDHClassStructure(JSC::LazyClassStructure::Initializer&); diff --git a/src/bun.js/bindings/node/crypto/JSECDHConstructor.cpp b/src/bun.js/bindings/node/crypto/JSECDHConstructor.cpp index ac25fc25e0..371bb94eef 100644 --- a/src/bun.js/bindings/node/crypto/JSECDHConstructor.cpp +++ b/src/bun.js/bindings/node/crypto/JSECDHConstructor.cpp @@ -71,7 +71,8 @@ JSC_DEFINE_HOST_FUNCTION(constructECDH, (JSC::JSGlobalObject * globalObject, JSC auto* zigGlobalObject = defaultGlobalObject(globalObject); JSC::Structure* structure = zigGlobalObject->m_JSECDHClassStructure.get(zigGlobalObject); - return JSC::JSValue::encode(JSECDH::create(vm, structure, globalObject, WTFMove(key))); + const EC_GROUP* group = key.getGroup(); + return JSC::JSValue::encode(JSECDH::create(vm, structure, globalObject, WTFMove(key), group)); } JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) @@ -85,18 +86,18 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalO RETURN_IF_EXCEPTION(scope, {}); JSValue keyValue = callFrame->argument(0); - JSValue keyEncValue = callFrame->argument(2); - auto* keyView = getArrayBufferOrView(lexicalGlobalObject, scope, keyValue, "key"_s, keyEncValue); + JSValue inEncValue = callFrame->argument(2); + auto* keyView = getArrayBufferOrView(lexicalGlobalObject, scope, keyValue, "key"_s, inEncValue); RETURN_IF_EXCEPTION(scope, {}); auto buffer = keyView->span(); - if (buffer.size() == 0) - return JSValue::encode(JSC::jsEmptyString(vm)); + JSValue formatValue = callFrame->argument(4); + point_conversion_form_t form = JSECDH::getFormat(lexicalGlobalObject, scope, formatValue); + RETURN_IF_EXCEPTION(scope, {}); - auto curveName = callFrame->argument(1).toWTFString(lexicalGlobalObject); - if (scope.exception()) - return encodedJSValue(); + auto curveName = curveValue.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); int nid = OBJ_sn2nid(curveName.utf8().data()); if (nid == NID_undef) @@ -113,27 +114,30 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalO const unsigned char* key_data = buffer.data(); size_t key_length = buffer.size(); - if (!EC_POINT_oct2point(group, point, key_data, key_length, nullptr)) - return throwVMError(lexicalGlobalObject, scope, "Failed to convert Buffer to EC_POINT"_s); + if (!point.setFromBuffer({ key_data, key_length }, group)) { + return Bun::ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "Failed to convert Buffer to EC_POINT"_s); + } - uint32_t form = callFrame->argument(2).toUInt32(lexicalGlobalObject); - if (scope.exception()) - return encodedJSValue(); + size_t size = EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (size == 0) { + return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "Failed to get public key length"_s); + } - size_t size = EC_POINT_point2oct(group, point, static_cast(form), nullptr, 0, nullptr); - if (size == 0) - return throwVMError(lexicalGlobalObject, scope, "Failed to calculate buffer size"_s); + WTF::Vector buf; + if (!buf.tryGrow(size)) { + throwOutOfMemoryError(lexicalGlobalObject, scope); + return JSValue::encode({}); + } - auto buf = ArrayBuffer::createUninitialized(size, 1); - if (!EC_POINT_point2oct(group, point, static_cast(form), reinterpret_cast(buf->data()), size, nullptr)) - return throwVMError(lexicalGlobalObject, scope, "Failed to convert EC_POINT to Buffer"_s); + if (!EC_POINT_point2oct(group, point, form, buf.data(), buf.size(), nullptr)) { + return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "Failed to get public key"_s); + } - auto* result = JSC::JSUint8Array::create(lexicalGlobalObject, reinterpret_cast(lexicalGlobalObject)->JSBufferSubclassStructure(), WTFMove(buf), 0, size); + JSValue outEncValue = callFrame->argument(3); + BufferEncodingType outEnc = getEncodingDefaultBuffer(lexicalGlobalObject, scope, outEncValue); + RETURN_IF_EXCEPTION(scope, {}); - if (!result) - return throwVMError(lexicalGlobalObject, scope, "Failed to allocate result buffer"_s); - - return JSValue::encode(result); + return StringBytes::encode(lexicalGlobalObject, scope, buf.span(), outEnc); } } // namespace Bun diff --git a/src/bun.js/bindings/node/crypto/JSECDHPrototype.cpp b/src/bun.js/bindings/node/crypto/JSECDHPrototype.cpp index ffec7c3aaf..92ee2718fd 100644 --- a/src/bun.js/bindings/node/crypto/JSECDHPrototype.cpp +++ b/src/bun.js/bindings/node/crypto/JSECDHPrototype.cpp @@ -53,16 +53,15 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncGenerateKeys, (JSC::JSGlobalObject * glo } // Get a copy of the key we can modify - auto keyImpl = ecdh->key().clone(); - if (!keyImpl.generate()) { + if (!ecdh->m_key.generate()) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to generate ECDH key pair"_s); return {}; } - // Update the instance with the new key - ecdh->setKey(WTFMove(keyImpl)); + JSValue encodingValue = callFrame->argument(0); + JSValue formatValue = callFrame->argument(1); - return JSValue::encode(jsUndefined()); + return ecdh->getPublicKey(globalObject, scope, encodingValue, formatValue); } JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncComputeSecret, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -95,22 +94,13 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncComputeSecret, (JSC::JSGlobalObject * gl } // Validate that we have a valid key pair - { - ncrypto::MarkPopErrorOnReturn markPopErrorOnReturn; - if (!ecdh->key().checkKey()) { - return Bun::ERR::CRYPTO_INVALID_KEYPAIR(scope, globalObject); - } - } - - // Get the group from our key - const EC_GROUP* group = ecdh->key().getGroup(); - if (!group) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to get EC_GROUP from key"_s); - return {}; + ncrypto::MarkPopErrorOnReturn markPopErrorOnReturn; + if (!ecdh->m_key.checkKey()) { + return Bun::ERR::CRYPTO_INVALID_KEYPAIR(scope, globalObject); } // Create an EC_POINT from the buffer - auto pubPoint = ncrypto::ECPointPointer::New(group); + auto pubPoint = ncrypto::ECPointPointer::New(ecdh->m_group); if (!pubPoint) { return Bun::ERR::CRYPTO_ECDH_INVALID_PUBLIC_KEY(scope, globalObject); } @@ -122,23 +112,22 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncComputeSecret, (JSC::JSGlobalObject * gl .len = keySpan.size() }; - if (!pubPoint.setFromBuffer(buffer, group)) { + if (!pubPoint.setFromBuffer(buffer, ecdh->m_group)) { return Bun::ERR::CRYPTO_ECDH_INVALID_PUBLIC_KEY(scope, globalObject); } // Compute the field size - int fieldSize = EC_GROUP_get_degree(group); + int fieldSize = EC_GROUP_get_degree(ecdh->m_group); size_t outLen = (fieldSize + 7) / 8; - // Allocate a buffer for the result - auto result = JSC::ArrayBuffer::tryCreate(outLen, 1); - if (!result) { - throwError(globalObject, scope, ErrorCode::ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate buffer for ECDH secret"_s); + WTF::Vector secret; + if (!secret.tryGrow(outLen)) { + throwOutOfMemoryError(globalObject, scope); return {}; } // Compute the shared secret - if (!ECDH_compute_key(result->data(), result->byteLength(), pubPoint, ecdh->key().get(), nullptr)) { + if (!ECDH_compute_key(secret.data(), secret.size(), pubPoint, ecdh->m_key.get(), nullptr)) { return Bun::ERR::CRYPTO_OPERATION_FAILED(scope, globalObject, "Failed to compute ECDH key"_s); } @@ -146,11 +135,8 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncComputeSecret, (JSC::JSGlobalObject * gl BufferEncodingType outputEncodingType = Bun::getEncodingDefaultBuffer(globalObject, scope, outputEncodingValue); RETURN_IF_EXCEPTION(scope, {}); - // Create a span from the result data for encoding - std::span resultSpan(static_cast(result->data()), outLen); - // Return the encoded result - return StringBytes::encode(globalObject, scope, resultSpan, outputEncodingType); + return StringBytes::encode(globalObject, scope, secret.span(), outputEncodingType); } JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncGetPublicKey, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -165,74 +151,11 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncGetPublicKey, (JSC::JSGlobalObject * glo } // Get encoding parameter - first argument could be encoding or format - JSC::JSValue encodingValue; - JSC::JSValue formatValue; - - if (callFrame->argumentCount() >= 2) { - // If there are at least 2 arguments, first is encoding, second is format - encodingValue = callFrame->argument(0); - formatValue = callFrame->argument(1); - } else if (callFrame->argumentCount() == 1) { - // If only one argument, check if it's a number (format) or string (encoding) - JSC::JSValue arg = callFrame->argument(0); - if (arg.isNumber()) { - formatValue = arg; - } else { - encodingValue = arg; - } - } + JSC::JSValue encodingValue = callFrame->argument(0); + JSC::JSValue formatValue = callFrame->argument(1); // Get the format parameter (default to uncompressed format if not provided) - point_conversion_form_t form = POINT_CONVERSION_UNCOMPRESSED; - if (formatValue.isUInt32()) { - form = static_cast(formatValue.asUInt32()); - // Validate the form is a valid conversion form - if (form != POINT_CONVERSION_COMPRESSED && form != POINT_CONVERSION_UNCOMPRESSED && form != POINT_CONVERSION_HYBRID) { - throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_VALUE, "Invalid point conversion format specified"_s); - return {}; - } - } else if (!formatValue.isUndefined() && !formatValue.isNull()) { - throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "Format argument must be a valid point conversion format"_s); - return {}; - } - - // Get the group and public key - const auto group = ecdh->key().getGroup(); - const auto pubKey = ecdh->key().getPublicKey(); - if (!pubKey) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to get ECDH public key"_s); - return {}; - } - - // Calculate the length needed for the result - size_t bufLen = EC_POINT_point2oct(group, pubKey, form, nullptr, 0, nullptr); - if (bufLen == 0) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to determine size for public key encoding"_s); - return {}; - } - - // Create a buffer to hold the result - auto result = JSC::ArrayBuffer::tryCreate(bufLen, 1); - if (!result) { - throwError(globalObject, scope, ErrorCode::ERR_MEMORY_ALLOCATION_FAILED, "Failed to allocate buffer for public key"_s); - return {}; - } - - // Encode the point to the buffer - if (EC_POINT_point2oct(group, pubKey, form, static_cast(result->data()), bufLen, nullptr) == 0) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to encode public key"_s); - return {}; - } - - // Handle output encoding if provided - BufferEncodingType encodingType = Bun::getEncodingDefaultBuffer(globalObject, scope, encodingValue); - RETURN_IF_EXCEPTION(scope, {}); - - // Create a span from the result data for encoding - std::span resultSpan(static_cast(result->data()), bufLen); - - // Return the encoded result - return StringBytes::encode(globalObject, scope, resultSpan, encodingType); + return ecdh->getPublicKey(globalObject, scope, encodingValue, formatValue); } JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncGetPrivateKey, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) @@ -247,7 +170,7 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncGetPrivateKey, (JSC::JSGlobalObject * gl } // Get the private key as a BIGNUM - const BIGNUM* privKey = ecdh->key().getPrivateKey(); + const BIGNUM* privKey = ecdh->m_key.getPrivateKey(); if (!privKey) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to get ECDH private key"_s); return {}; @@ -302,13 +225,6 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPublicKey, (JSC::JSGlobalObject * glo JSC::JSValue keyValue = callFrame->argument(0); JSC::JSValue encodingValue = callFrame->argument(1); - // Get the group from our key - const EC_GROUP* group = ecdh->key().getGroup(); - if (!group) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_STATE, "Failed to get EC_GROUP from key"_s); - return {}; - } - // Convert the input to a buffer with encoding if provided auto* bufferValue = Bun::getArrayBufferOrView(globalObject, scope, keyValue, "key"_s, encodingValue); RETURN_IF_EXCEPTION(scope, {}); @@ -318,8 +234,10 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPublicKey, (JSC::JSGlobalObject * glo return {}; } + ncrypto::MarkPopErrorOnReturn markPopErrorOnReturn; + // Create an EC_POINT from the buffer - auto pubPoint = ncrypto::ECPointPointer::New(group); + auto pubPoint = ncrypto::ECPointPointer::New(ecdh->m_group); if (!pubPoint) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to allocate EC_POINT for public key"_s); return {}; @@ -332,27 +250,17 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPublicKey, (JSC::JSGlobalObject * glo .len = keySpan.size() }; - if (!pubPoint.setFromBuffer(buffer, group)) { + if (!pubPoint.setFromBuffer(buffer, ecdh->m_group)) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to set EC_POINT from buffer"_s); return {}; } - // Clone the existing key, set the public key, then update the instance - auto newKey = ecdh->key().clone(); - if (!newKey) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to clone EC key"_s); - return {}; - } - // Set the public key - if (!newKey.setPublicKey(pubPoint)) { + if (!ecdh->m_key.setPublicKey(pubPoint)) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to set EC_POINT as the public key"_s); return {}; } - // Replace the old key with the new one - ecdh->setKey(WTFMove(newKey)); - // Return this for chaining return JSValue::encode(callFrame->thisValue()); } @@ -394,13 +302,12 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPrivateKey, (JSC::JSGlobalObject * gl } // Validate the key is valid for the curve - if (!isKeyValidForCurve(ecdh->key().getGroup(), privateKey)) { - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, "Private key is not valid for the specified curve"_s); - return {}; + if (!isKeyValidForCurve(ecdh->m_group, privateKey)) { + return Bun::ERR::CRYPTO_INVALID_KEYTYPE(scope, globalObject, "Private key is not valid for specified curve"_s); } // Clone the existing key - auto newKey = ecdh->key().clone(); + auto newKey = ecdh->m_key.clone(); if (!newKey) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to clone EC key"_s); return {}; @@ -420,14 +327,14 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPrivateKey, (JSC::JSGlobalObject * gl } // Create a new EC_POINT for the public key - auto pubPoint = ncrypto::ECPointPointer::New(ecdh->key().getGroup()); + auto pubPoint = ncrypto::ECPointPointer::New(ecdh->m_group); if (!pubPoint) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to allocate EC_POINT for public key"_s); return {}; } // Compute the public key point from the private key - if (!pubPoint.mul(ecdh->key().getGroup(), privKey)) { + if (!pubPoint.mul(ecdh->m_group, privKey)) { throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_OPERATION_FAILED, "Failed to compute public key from private key"_s); return {}; } @@ -439,7 +346,8 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHProtoFuncSetPrivateKey, (JSC::JSGlobalObject * gl } // Replace the old key with the new one - ecdh->setKey(WTFMove(newKey)); + ecdh->m_key = WTFMove(newKey); + ecdh->m_group = ecdh->m_key.getGroup(); // Return this for chaining return JSValue::encode(callFrame->thisValue()); diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index f78a46ef1e..fa8392082d 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -418,7 +418,8 @@ pub export fn Bun__GlobalObject__hasIPC(global: *JSGlobalObject) bool { return global.bunVM().ipc != null; } -pub extern fn Bun__Process__queueNextTick1(*ZigGlobalObject, JSValue, JSValue) void; +pub extern fn Bun__Process__queueNextTick1(*ZigGlobalObject, func: JSValue, JSValue) void; +pub extern fn Bun__Process__queueNextTick2(*ZigGlobalObject, func: JSValue, JSValue, JSValue) void; comptime { const Bun__Process__send = JSC.toJSHostFunction(Bun__Process__send_); diff --git a/src/bun.js/node/node_crypto_binding.zig b/src/bun.js/node/node_crypto_binding.zig index df9a40a5d4..37acb601a9 100644 --- a/src/bun.js/node/node_crypto_binding.zig +++ b/src/bun.js/node/node_crypto_binding.zig @@ -22,15 +22,15 @@ const random = struct { const max_range = 0xffff_ffff_ffff; fn randomInt(global: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const args = callFrame.arguments(); + var min_value, var max_value, var callback = callFrame.argumentsAsArray(3); - const min_value, const max_value, const callback, const min_specified = args: { - if (args.len == 2) { - break :args .{ JSC.jsNumber(0), args[0], args[1], false }; - } - - break :args .{ args[0], args[1], args[2], true }; - }; + var min_specified = true; + if (max_value.isUndefined() or max_value.isFunction()) { + callback = max_value; + max_value = min_value; + min_value = JSValue.jsNumber(0); + min_specified = false; + } if (!callback.isUndefined()) { _ = try validators.validateFunction(global, "callback", callback); @@ -60,7 +60,14 @@ const random = struct { return global.ERR_OUT_OF_RANGE("The value of \"max\" is out of range. It must be <= {d}. Received {d}", .{ max_range, max - min }).throw(); } - return JSC.jsNumber(std.crypto.random.intRangeLessThan(i64, min, max)); + const res = std.crypto.random.intRangeLessThan(i64, min, max); + + if (!callback.isUndefined()) { + callback.callNextTick(global, [2]JSValue{ .undefined, JSValue.jsNumber(res) }); + return JSValue.jsUndefined(); + } + + return JSValue.jsNumber(res); } fn randomUUID(global: *JSGlobalObject, callFrame: *JSC.CallFrame) JSError!JSValue { diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 43a0c3cc98..482fd3da6d 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -48,7 +48,7 @@ const { pbkdf2: _pbkdf2, pbkdf2Sync: _pbkdf2Sync, timingSafeEqual: _timingSafeEqual, - randomInt: _randomInt, + randomInt, randomUUID: _randomUUID, randomBytes: _randomBytes, randomFillSync, @@ -2534,23 +2534,6 @@ for (const rng of ["pseudoRandomBytes", "prng", "rng"]) { }); } -function randomInt(min, max, callback) { - let res; - if (typeof max === "undefined" || typeof max === "function") { - callback = max; - res = _randomInt(min, callback); - } else { - res = _randomInt(min, max, callback); - } - - if (callback !== undefined) { - // Crypto random promise job is guaranteed to resolve. - process.nextTick(callback, undefined, res); - } - - return res; -} - crypto_exports.randomInt = randomInt; function randomFill(buf, offset, size, callback) { diff --git a/test/js/node/crypto/crypto-random.test.ts b/test/js/node/crypto/crypto-random.test.ts index 664f7e53a2..7c4673b701 100644 --- a/test/js/node/crypto/crypto-random.test.ts +++ b/test/js/node/crypto/crypto-random.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; -import { randomInt } from "crypto"; +import { randomInt, randomBytes } from "crypto"; -describe("randomInt args validation", async () => { +describe("randomInt args validation", () => { it("default min is 0 so max should be greater than 0", () => { expect(() => randomInt(-1)).toThrow(RangeError); expect(() => randomInt(0)).toThrow(RangeError); @@ -28,4 +28,32 @@ describe("randomInt args validation", async () => { it("accept large negative numbers", () => { expect(() => randomInt(-Number.MAX_SAFE_INTEGER, -Number.MAX_SAFE_INTEGER + 1)).not.toThrow(RangeError); }); + + it("should return undefined if called with callback", async () => { + const { resolve, promise } = Promise.withResolvers(); + + expect( + randomInt(1, 2, (err, num) => { + expect(err).toBeUndefined(); + expect(num).toBe(1); + resolve(); + }), + ).toBeUndefined(); + + await promise; + }); +}); + +describe("randomBytes", () => { + it("error should be null", async () => { + const { resolve, promise } = Promise.withResolvers(); + + randomBytes(10, (err, buf) => { + expect(err).toBeNull(); + expect(buf).toBeInstanceOf(Buffer); + resolve(); + }); + + await promise; + }); }); diff --git a/test/js/node/crypto/ecdh.test.ts b/test/js/node/crypto/ecdh.test.ts new file mode 100644 index 0000000000..e1e53d1eb2 --- /dev/null +++ b/test/js/node/crypto/ecdh.test.ts @@ -0,0 +1,240 @@ +import { test, expect } from "bun:test"; +import { createECDH, ECDH, getCurves } from "node:crypto"; + +// Helper function to generate test key pairs for various curves +function generateTestKeyPairs() { + const curves = getCurves(); + const keys = {}; + + for (const curve of curves) { + const ecdh = createECDH(curve); + ecdh.generateKeys(); + + keys[curve] = { + compressed: ecdh.getPublicKey("hex", "compressed"), + uncompressed: ecdh.getPublicKey("hex", "uncompressed"), + instance: ecdh, + }; + } + + return keys; +} + +// Test creating an ECDH instance +test("crypto.createECDH - creates ECDH instance", () => { + // Get a supported curve from the available curves + const curve = getCurves()[0]; + const ecdh = createECDH(curve); + expect(ecdh).toBeInstanceOf(ECDH); +}); + +// Test that unsupported curves throw errors +test("crypto.createECDH - throws for unsupported curves", () => { + expect(() => createECDH("definitely-not-a-real-curve-name")).toThrow(); +}); + +// Test ECDH key generation for each supported curve +test("ECDH - generateKeys works on all supported curves", () => { + const curves = getCurves(); + for (const curve of curves) { + const ecdh = createECDH(curve); + const keys = ecdh.generateKeys(); + expect(keys).toBeInstanceOf(Buffer); + expect(keys.length).toBeGreaterThan(0); + } +}); + +// Test ECDH shared secret computation (use the first available curve) +test("ECDH - computeSecret generates same secret for both parties", () => { + const curve = getCurves()[0]; + const alice = createECDH(curve); + const bob = createECDH(curve); + + // Generate key pairs + const alicePubKey = alice.generateKeys(); + const bobPubKey = bob.generateKeys(); + + // Compute shared secrets + const aliceSecret = alice.computeSecret(bobPubKey); + const bobSecret = bob.computeSecret(alicePubKey); + + // Both shared secrets should be the same + expect(aliceSecret.toString("hex")).toBe(bobSecret.toString("hex")); +}); + +// Test key formats +test("ECDH - supports different key formats", () => { + const curve = getCurves()[0]; + const ecdh = createECDH(curve); + ecdh.generateKeys(); + + // Get public key in different formats + const publicKeyHex = ecdh.getPublicKey("hex"); + const publicKeyBase64 = ecdh.getPublicKey("base64"); + const publicKeyBuffer = ecdh.getPublicKey(); + + expect(typeof publicKeyHex).toBe("string"); + expect(typeof publicKeyBase64).toBe("string"); + expect(publicKeyBuffer).toBeInstanceOf(Buffer); +}); + +// Test key compression formats +test("ECDH - supports compressed and uncompressed formats", () => { + const curve = getCurves()[0]; + const ecdh = createECDH(curve); + ecdh.generateKeys(); + + // Get public key in different compression formats + const uncompressedKey = ecdh.getPublicKey("hex", "uncompressed"); + const compressedKey = ecdh.getPublicKey("hex", "compressed"); + + expect(typeof uncompressedKey).toBe("string"); + expect(typeof compressedKey).toBe("string"); + // Compressed key should be shorter + expect(compressedKey.length).toBeLessThan(uncompressedKey.length); +}); + +// Test exporting and importing private keys +test("ECDH - exports and imports private keys", () => { + const curve = getCurves()[0]; + const ecdh = createECDH(curve); + ecdh.generateKeys(); + + // Export private key + const privateKeyHex = ecdh.getPrivateKey("hex"); + + // Create new instance + const ecdh2 = createECDH(curve); + + // Import private key + ecdh2.setPrivateKey(privateKeyHex, "hex"); + + // Both instances should generate the same public key + expect(ecdh2.getPublicKey("hex")).toBe(ecdh.getPublicKey("hex")); +}); + +// Test setting public key +test("ECDH - can set public key and compute secret", () => { + const curve = getCurves()[0]; + const alice = createECDH(curve); + const bob = createECDH(curve); + + // Generate keys + alice.generateKeys(); + bob.generateKeys(); + + // Get public keys + const alicePubKey = alice.getPublicKey(); + const bobPubKey = bob.getPublicKey(); + + // Create new instances + const aliceClone = createECDH(curve); + const bobClone = createECDH(curve); + + // Set private keys + aliceClone.setPrivateKey(alice.getPrivateKey()); + bobClone.setPrivateKey(bob.getPrivateKey()); + + // Compute secrets using original public keys + const secret1 = aliceClone.computeSecret(bobPubKey); + const secret2 = bobClone.computeSecret(alicePubKey); + + // Secrets should match + expect(secret1.toString("hex")).toBe(secret2.toString("hex")); +}); + +// Test error handling +test("ECDH - throws when computing secret with invalid key", () => { + const curve = getCurves()[0]; + const ecdh = createECDH(curve); + ecdh.generateKeys(); + + // Invalid public key + const invalidKey = Buffer.from("invalid key"); + + // Should throw error + expect(() => ecdh.computeSecret(invalidKey)).toThrow(); +}); + +// Test all curves with basic operations +test("ECDH - basic operations work on all supported curves", () => { + const curves = getCurves(); + + for (const curve of curves) { + const alice = createECDH(curve); + const bob = createECDH(curve); + + // Generate keys + alice.generateKeys(); + bob.generateKeys(); + + // Compute shared secret + const aliceSecret = alice.computeSecret(bob.getPublicKey()); + const bobSecret = bob.computeSecret(alice.getPublicKey()); + + // Check that secrets match + expect(aliceSecret.toString("hex")).toBe(bobSecret.toString("hex")); + } +}); + +// Tests for ECDH.convertKey functionality +test("ECDH.convertKey - converts between compressed and uncompressed formats", () => { + const testKeys = generateTestKeyPairs(); + + for (const curve of Object.keys(testKeys)) { + const compressed = testKeys[curve].compressed; + const uncompressed = testKeys[curve].uncompressed; + + // Test compressed to uncompressed + const convertedToUncompressed = ECDH.convertKey(compressed, curve, "hex", "hex", "uncompressed"); + expect(convertedToUncompressed).toBe(uncompressed); + + // Test uncompressed to compressed + const convertedToCompressed = ECDH.convertKey(uncompressed, curve, "hex", "hex", "compressed"); + expect(convertedToCompressed).toBe(compressed); + } +}); + +test("ECDH.convertKey - supports different input and output encodings", () => { + const testKeys = generateTestKeyPairs(); + + const compressedHex = testKeys["prime256v1"].compressed; + + // Convert from hex to buffer + const convertedToBuffer = ECDH.convertKey(compressedHex, "prime256v1", "hex", "buffer", "compressed"); + expect(convertedToBuffer).toBeInstanceOf(Buffer); + expect(convertedToBuffer.toString("hex")).toBe(compressedHex); + + // Convert from hex to base64 + const convertedToBase64 = ECDH.convertKey(compressedHex, "prime256v1", "hex", "base64", "compressed"); + expect(typeof convertedToBase64).toBe("string"); + expect(Buffer.from(convertedToBase64, "base64").toString("hex")).toBe(compressedHex); +}); + +test("ECDH.convertKey - throws on invalid input", () => { + // Invalid key + expect(() => { + ECDH.convertKey("invalid-key", "prime256v1", "hex", "hex", "compressed"); + }).toThrow("The argument 'encoding' is invalid for data of length 11. Received 'hex'"); + + // Invalid curve + expect(() => { + ECDH.convertKey( + "0102030405", // Some hex data + "not-a-valid-curve", + "hex", + "hex", + "compressed", + ); + }).toThrow("Invalid EC curve name"); + + // Invalid input encoding + expect(() => { + ECDH.convertKey("0102030405", "prime256v1", "invalid-encoding", "hex", "compressed"); + }).toThrow("Unknown encoding: invalid-encoding"); + + // Invalid format + expect(() => { + ECDH.convertKey("0102030405", "prime256v1", "hex", "hex", "invalid-format"); + }).toThrow("Invalid ECDH format: invalid-format"); +});