From 1a68ce05dc620f3588626a67dc89c287dfa0cbfe Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Fri, 7 Mar 2025 20:53:06 -0800 Subject: [PATCH] Add a few passing tests for `node:crypto` (#17987) --- src/bun.js/bindings/ErrorCode.cpp | 14 ++- src/bun.js/bindings/ErrorCode.h | 1 + src/bun.js/bindings/ErrorCode.ts | 1 + src/bun.js/node/node_crypto_binding.zig | 31 ++++- src/bun.js/webcore.zig | 28 +---- src/js/node/crypto.ts | 37 ++---- src/jsc.zig | 1 + ...ypto-keygen-async-encrypted-private-key.js | 67 ++++++++++ ...-explicit-elliptic-curve-encrypted-p256.js | 57 +++++++++ ...nc-explicit-elliptic-curve-encrypted.js.js | 53 ++++++++ ...ync-named-elliptic-curve-encrypted-p256.js | 56 +++++++++ ...en-async-named-elliptic-curve-encrypted.js | 53 ++++++++ .../parallel/test-crypto-keygen-async-rsa.js | 62 +++++++++ ...rypto-keygen-empty-passphrase-no-prompt.js | 54 ++++++++ ...to-keygen-invalid-parameter-encoding-ec.js | 27 ++++ .../test-crypto-timing-safe-equal.js | 119 ++++++++++++++++++ test/js/web/web-globals.test.js | 2 +- 17 files changed, 607 insertions(+), 56 deletions(-) create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-async-rsa.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js create mode 100644 test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-ec.js create mode 100644 test/js/node/test/sequential/test-crypto-timing-safe-equal.js diff --git a/src/bun.js/bindings/ErrorCode.cpp b/src/bun.js/bindings/ErrorCode.cpp index 270380e662..4143ee71a3 100644 --- a/src/bun.js/bindings/ErrorCode.cpp +++ b/src/bun.js/bindings/ErrorCode.cpp @@ -1010,8 +1010,11 @@ JSC::EncodedJSValue CRYPTO_INVALID_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlo JSC::EncodedJSValue CRYPTO_JWK_UNSUPPORTED_CURVE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& curve) { - auto message = makeString("Unsupported JWK EC curve: "_s, curve); - throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, message)); + WTF::StringBuilder builder; + builder.append("Unsupported JWK EC curve: "_s); + builder.append(curve); + builder.append('.'); + throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_JWK_UNSUPPORTED_CURVE, builder.toString())); return {}; } @@ -1072,6 +1075,13 @@ JSC::EncodedJSValue CRYPTO_HASH_UPDATE_FAILED(JSC::ThrowScope& throwScope, JSC:: return {}; } +JSC::EncodedJSValue CRYPTO_TIMING_SAFE_EQUAL_LENGTH(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject) +{ + auto message = "Input buffers must have the same byte length"_s; + scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH, message)); + return {}; +} + JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message) { throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_MISSING_PASSPHRASE, message)); diff --git a/src/bun.js/bindings/ErrorCode.h b/src/bun.js/bindings/ErrorCode.h index 92b84685f8..6a94620d91 100644 --- a/src/bun.js/bindings/ErrorCode.h +++ b/src/bun.js/bindings/ErrorCode.h @@ -98,6 +98,7 @@ JSC::EncodedJSValue CRYPTO_INVALID_DIGEST(JSC::ThrowScope& throwScope, JSC::JSGl JSC::EncodedJSValue CRYPTO_HASH_FINALIZED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue CRYPTO_HASH_UPDATE_FAILED(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); JSC::EncodedJSValue MISSING_PASSPHRASE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral message); +JSC::EncodedJSValue CRYPTO_TIMING_SAFE_EQUAL_LENGTH(JSC::ThrowScope&, JSC::JSGlobalObject*); // URL diff --git a/src/bun.js/bindings/ErrorCode.ts b/src/bun.js/bindings/ErrorCode.ts index a161069547..7660bcbdc1 100644 --- a/src/bun.js/bindings/ErrorCode.ts +++ b/src/bun.js/bindings/ErrorCode.ts @@ -35,6 +35,7 @@ const errors: ErrorCodeMapping = [ ["ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS", Error], ["ERR_CRYPTO_HASH_FINALIZED", Error], ["ERR_CRYPTO_HASH_UPDATE_FAILED", Error], + ["ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH", RangeError], ["ERR_MISSING_PASSPHRASE", TypeError], ["ERR_DLOPEN_FAILED", Error], ["ERR_ENCODING_INVALID_ENCODED_DATA", TypeError], diff --git a/src/bun.js/node/node_crypto_binding.zig b/src/bun.js/node/node_crypto_binding.zig index 62ae408a7b..453ee16855 100644 --- a/src/bun.js/node/node_crypto_binding.zig +++ b/src/bun.js/node/node_crypto_binding.zig @@ -102,13 +102,34 @@ fn pbkdf2Sync(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JS return out_arraybuffer; } -pub fn createNodeCryptoBindingZig(global: *JSC.JSGlobalObject) JSC.JSValue { - const crypto = JSC.JSValue.createEmptyObject(global, 4); +pub fn timingSafeEqual(global: *JSGlobalObject, callFrame: *JSC.CallFrame) JSError!JSValue { + const l_value, const r_value = callFrame.argumentsAsArray(2); - crypto.put(global, bun.String.init("pbkdf2"), JSC.JSFunction.create(global, "pbkdf2", pbkdf2, 5, .{})); - crypto.put(global, bun.String.init("pbkdf2Sync"), JSC.JSFunction.create(global, "pbkdf2Sync", pbkdf2Sync, 5, .{})); - crypto.put(global, bun.String.init("randomInt"), JSC.JSFunction.create(global, "randomInt", randomInt, 2, .{})); + const l_buf = l_value.asArrayBuffer(global) orelse { + return global.ERR_INVALID_ARG_TYPE("The \"buf1\" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.", .{}).throw(); + }; + const l = l_buf.byteSlice(); + + const r_buf = r_value.asArrayBuffer(global) orelse { + return global.ERR_INVALID_ARG_TYPE("The \"buf2\" argument must be an instance of ArrayBuffer, Buffer, TypedArray, or DataView.", .{}).throw(); + }; + const r = r_buf.byteSlice(); + + if (l.len != r.len) { + return global.ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH("Input buffers must have the same byte length", .{}).throw(); + } + + return JSC.jsBoolean(BoringSSL.CRYPTO_memcmp(l.ptr, r.ptr, l.len) == 0); +} + +pub fn createNodeCryptoBindingZig(global: *JSC.JSGlobalObject) JSC.JSValue { + const crypto = JSC.JSValue.createEmptyObject(global, 5); + + crypto.put(global, String.init("pbkdf2"), JSC.JSFunction.create(global, "pbkdf2", pbkdf2, 5, .{})); + crypto.put(global, String.init("pbkdf2Sync"), JSC.JSFunction.create(global, "pbkdf2Sync", pbkdf2Sync, 5, .{})); + crypto.put(global, String.init("randomInt"), JSC.JSFunction.create(global, "randomInt", randomInt, 2, .{})); crypto.put(global, String.init("randomUUID"), JSC.JSFunction.create(global, "randomUUID", randomUUID, 1, .{})); + crypto.put(global, String.init("timingSafeEqual"), JSC.JSFunction.create(global, "timingSafeEqual", timingSafeEqual, 2, .{})); return crypto; } diff --git a/src/bun.js/webcore.zig b/src/bun.js/webcore.zig index 6c16aec506..2f50ebff9d 100644 --- a/src/bun.js/webcore.zig +++ b/src/bun.js/webcore.zig @@ -539,28 +539,8 @@ pub const Crypto = struct { return globalThis.ERR_CRYPTO_INVALID_SCRYPT_PARAMS(message, fmt).throw(); } - pub fn timingSafeEqual(_: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { - const arguments = callframe.arguments_old(2).slice(); - - if (arguments.len < 2) { - return globalThis.throwInvalidArguments("Expected 2 typed arrays but got nothing", .{}); - } - - const array_buffer_a = arguments[0].asArrayBuffer(globalThis) orelse { - return globalThis.throwInvalidArguments("Expected typed array but got {s}", .{@tagName(arguments[0].jsType())}); - }; - const a = array_buffer_a.byteSlice(); - - const array_buffer_b = arguments[1].asArrayBuffer(globalThis) orelse { - return globalThis.throwInvalidArguments("Expected typed array but got {s}", .{@tagName(arguments[1].jsType())}); - }; - const b = array_buffer_b.byteSlice(); - - const len = a.len; - if (b.len != len) { - return globalThis.throw("Input buffers must have the same byte length", .{}); - } - return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); + pub fn timingSafeEqual(_: *@This(), global: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue { + return JSC.Node.Crypto.timingSafeEqual(global, callframe); } pub fn timingSafeEqualWithoutTypeChecks( @@ -574,10 +554,10 @@ pub const Crypto = struct { const len = a.len; if (b.len != len) { - return globalThis.throw("Input buffers must have the same byte length", .{}); + return globalThis.ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH("Input buffers must have the same byte length", .{}).throw(); } - return JSC.jsBoolean(len == 0 or bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); + return JSC.jsBoolean(bun.BoringSSL.c.CRYPTO_memcmp(a.ptr, b.ptr, len) == 0); } pub fn getRandomValues( diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index dc4ac358a2..54b2be097e 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -47,9 +47,10 @@ const { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_HYBRID, POINT_CONVERSION_U const { randomInt: _randomInt, + pbkdf2: _pbkdf2, + pbkdf2Sync: _pbkdf2Sync, + timingSafeEqual: _timingSafeEqual, randomUUID: _randomUUID, - pbkdf2: pbkdf2_, - pbkdf2Sync: pbkdf2Sync_, } = $zig("node_crypto_binding.zig", "createNodeCryptoBindingZig"); const { validateObject, validateString, validateInt32 } = require("internal/validators"); @@ -493,7 +494,7 @@ function pbkdf2(password, salt, iterations, keylen, digest, callback) { digest = undefined; } - const promise = pbkdf2_(password, salt, iterations, keylen, digest, callback); + const promise = _pbkdf2(password, salt, iterations, keylen, digest, callback); if (callback) { promise.then( result => callback(null, result), @@ -506,7 +507,7 @@ function pbkdf2(password, salt, iterations, keylen, digest, callback) { } function pbkdf2Sync(password, salt, iterations, keylen, digest) { - return pbkdf2Sync_(password, salt, iterations, keylen, digest); + return _pbkdf2Sync(password, salt, iterations, keylen, digest); } // node_modules/des.js/lib/des/utils.js @@ -4380,8 +4381,9 @@ var require_browser7 = __commonJS({ exports.DiffieHellmanGroup = exports.createDiffieHellmanGroup = exports.getDiffieHellman = getDiffieHellman; exports.createDiffieHellman = exports.DiffieHellman = createDiffieHellman; + // TODO: move entire function out of js in diffie-hellman pr exports.diffieHellman = function diffieHellman(options) { - validateObject(options); + validateObject(options, "options"); const { privateKey, publicKey } = options; @@ -10194,17 +10196,6 @@ var crypto_exports = require_crypto_browserify2(); var getRandomValues = array => crypto.getRandomValues(array), randomUUID = () => crypto.randomUUID(), - timingSafeEqual = - "timingSafeEqual" in crypto - ? (a, b) => { - let { byteLength: byteLengthA } = a, - { byteLength: byteLengthB } = b; - if (typeof byteLengthA != "number" || typeof byteLengthB != "number") - throw new TypeError("Input must be an array buffer view"); - if (byteLengthA !== byteLengthB) throw new RangeError("Input buffers must have the same length"); - return crypto.timingSafeEqual(a, b); - } - : void 0, scryptSync = "scryptSync" in crypto ? (password, salt, keylen, options) => { @@ -10229,16 +10220,14 @@ var getRandomValues = array => crypto.getRandomValues(array), } } : void 0; -timingSafeEqual && - (Object.defineProperty(timingSafeEqual, "name", { - value: "::bunternal::", - }), +scrypt && Object.defineProperty(scrypt, "name", { value: "::bunternal::", }), - Object.defineProperty(scryptSync, "name", { - value: "::bunternal::", - })); + scryptSync && + Object.defineProperty(scryptSync, "name", { + value: "::bunternal::", + }); class KeyObject { // we use $bunNativePtr so that util.types.isKeyObject can detect it @@ -10576,7 +10565,7 @@ crypto_exports.getCurves = getCurves; crypto_exports.getCipherInfo = getCipherInfo; crypto_exports.scrypt = scrypt; crypto_exports.scryptSync = scryptSync; -crypto_exports.timingSafeEqual = timingSafeEqual; +crypto_exports.timingSafeEqual = _timingSafeEqual; crypto_exports.webcrypto = webcrypto; crypto_exports.subtle = _subtle; crypto_exports.X509Certificate = X509Certificate; diff --git a/src/jsc.zig b/src/jsc.zig index 8af9ba76f0..c62d601dee 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -69,6 +69,7 @@ pub const Node = struct { pub const Util = struct { pub const parseArgs = @import("./bun.js/node/util/parse_args.zig").parseArgs; }; + pub const Crypto = @import("./bun.js/node/node_crypto_binding.zig"); }; const std = @import("std"); diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key.js b/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key.js new file mode 100644 index 0000000000..727cccc6f3 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-encrypted-private-key.js @@ -0,0 +1,67 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, +} = require('../common/crypto'); + +// Test async RSA key generation with an encrypted private key, but encoded as DER. +{ + generateKeyPair('rsa', { + publicExponent: 0x10001, + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'der' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'der', + cipher: 'aes-256-cbc', + passphrase: 'secret' + } + }, common.mustSucceed((publicKeyDER, privateKeyDER) => { + assert(Buffer.isBuffer(publicKeyDER)); + assertApproximateSize(publicKeyDER, 74); + + assert(Buffer.isBuffer(privateKeyDER)); + + // Since the private key is encrypted, signing shouldn't work anymore. + const publicKey = { + key: publicKeyDER, + type: 'pkcs1', + format: 'der', + }; + assert.throws(() => { + testSignVerify(publicKey, { + key: privateKeyDER, + format: 'der', + type: 'pkcs8' + }); + }, { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + // Signing should work with the correct password. + + const privateKey = { + key: privateKeyDER, + format: 'der', + type: 'pkcs8', + passphrase: 'secret' + }; + testEncryptDecrypt(publicKey, privateKey); + testSignVerify(publicKey, privateKey); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js new file mode 100644 index 0000000000..55aa3831c4 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted-p256.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + pkcs8EncExp, +} = require('../common/crypto'); + +const { hasOpenSSL3 } = require('../common/crypto'); + +// Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted +// private key with paramEncoding explicit. +{ + generateKeyPair('ec', { + namedCurve: 'P-256', + paramEncoding: 'explicit', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'top secret' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, pkcs8EncExp); + + // Since the private key is encrypted, signing shouldn't work anymore. + assert.throws(() => testSignVerify(publicKey, privateKey), + hasOpenSSL3 ? { + message: 'error:07880109:common libcrypto ' + + 'routines::interrupted or cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + testSignVerify(publicKey, { + key: privateKey, + passphrase: 'top secret' + }); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js new file mode 100644 index 0000000000..8a55d4338b --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-explicit-elliptic-curve-encrypted.js.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + sec1EncExp, + hasOpenSSL3, +} = require('../common/crypto'); + +{ + // Test async explicit elliptic curve key generation with an encrypted + // private key. + generateKeyPair('ec', { + namedCurve: 'prime256v1', + paramEncoding: 'explicit', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'sec1', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'secret' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, sec1EncExp('AES-128-CBC')); + + // Since the private key is encrypted, signing shouldn't work anymore. + assert.throws(() => testSignVerify(publicKey, privateKey), + hasOpenSSL3 ? { + message: 'error:07880109:common libcrypto ' + + 'routines::interrupted or cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' }); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js new file mode 100644 index 0000000000..4c11401d0f --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted-p256.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + pkcs8EncExp, + hasOpenSSL3, +} = require('../common/crypto'); + +// Test async elliptic curve key generation, e.g. for ECDSA, with an encrypted +// private key. +{ + generateKeyPair('ec', { + namedCurve: 'P-256', + paramEncoding: 'named', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'top secret' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, pkcs8EncExp); + + // Since the private key is encrypted, signing shouldn't work anymore. + assert.throws(() => testSignVerify(publicKey, privateKey), + hasOpenSSL3 ? { + message: 'error:07880109:common libcrypto ' + + 'routines::interrupted or cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + testSignVerify(publicKey, { + key: privateKey, + passphrase: 'top secret' + }); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js new file mode 100644 index 0000000000..0503ff7478 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-named-elliptic-curve-encrypted.js @@ -0,0 +1,53 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + spkiExp, + sec1EncExp, + hasOpenSSL3, +} = require('../common/crypto'); + +{ + // Test async named elliptic curve key generation with an encrypted + // private key. + generateKeyPair('ec', { + namedCurve: 'prime256v1', + paramEncoding: 'named', + publicKeyEncoding: { + type: 'spki', + format: 'pem' + }, + privateKeyEncoding: { + type: 'sec1', + format: 'pem', + cipher: 'aes-128-cbc', + passphrase: 'secret' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(typeof publicKey, 'string'); + assert.match(publicKey, spkiExp); + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, sec1EncExp('AES-128-CBC')); + + // Since the private key is encrypted, signing shouldn't work anymore. + assert.throws(() => testSignVerify(publicKey, privateKey), + hasOpenSSL3 ? { + message: 'error:07880109:common libcrypto ' + + 'routines::interrupted or cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + + testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' }); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-async-rsa.js b/test/js/node/test/parallel/test-crypto-keygen-async-rsa.js new file mode 100644 index 0000000000..c80d7d3349 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-async-rsa.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + generateKeyPair, +} = require('crypto'); +const { + assertApproximateSize, + testEncryptDecrypt, + testSignVerify, + pkcs1EncExp, + hasOpenSSL3, +} = require('../common/crypto'); + +// Test async RSA key generation with an encrypted private key. +{ + generateKeyPair('rsa', { + publicExponent: 0x10001, + modulusLength: 512, + publicKeyEncoding: { + type: 'pkcs1', + format: 'der' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: 'secret' + } + }, common.mustSucceed((publicKeyDER, privateKey) => { + assert(Buffer.isBuffer(publicKeyDER)); + assertApproximateSize(publicKeyDER, 74); + + assert.strictEqual(typeof privateKey, 'string'); + assert.match(privateKey, pkcs1EncExp('AES-256-CBC')); + + // Since the private key is encrypted, signing shouldn't work anymore. + const publicKey = { + key: publicKeyDER, + type: 'pkcs1', + format: 'der', + }; + const expectedError = hasOpenSSL3 ? { + name: 'Error', + message: 'error:07880109:common libcrypto routines::interrupted or ' + + 'cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }; + assert.throws(() => testSignVerify(publicKey, privateKey), expectedError); + + const key = { key: privateKey, passphrase: 'secret' }; + testEncryptDecrypt(publicKey, key); + testSignVerify(publicKey, key); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js b/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js new file mode 100644 index 0000000000..cb873ff047 --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-empty-passphrase-no-prompt.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { + createPrivateKey, + generateKeyPair, +} = require('crypto'); +const { + testSignVerify, + hasOpenSSL3, +} = require('../common/crypto'); + +// Passing an empty passphrase string should not cause OpenSSL's default +// passphrase prompt in the terminal. +// See https://github.com/nodejs/node/issues/35898. +for (const type of ['pkcs1', 'pkcs8']) { + generateKeyPair('rsa', { + modulusLength: 1024, + privateKeyEncoding: { + type, + format: 'pem', + cipher: 'aes-256-cbc', + passphrase: '' + } + }, common.mustSucceed((publicKey, privateKey) => { + assert.strictEqual(publicKey.type, 'public'); + + for (const passphrase of ['', Buffer.alloc(0)]) { + const privateKeyObject = createPrivateKey({ + passphrase, + key: privateKey + }); + assert.strictEqual(privateKeyObject.asymmetricKeyType, 'rsa'); + } + + // Encrypting with an empty passphrase is not the same as not encrypting + // the key, and not specifying a passphrase should fail when decoding it. + assert.throws(() => { + return testSignVerify(publicKey, privateKey); + }, hasOpenSSL3 ? { + name: 'Error', + code: 'ERR_OSSL_CRYPTO_INTERRUPTED_OR_CANCELLED', + message: 'error:07880109:common libcrypto routines::interrupted or cancelled' + } : { + name: 'TypeError', + code: 'ERR_MISSING_PASSPHRASE', + message: 'Passphrase required for encrypted key' + }); + })); +} diff --git a/test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-ec.js b/test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-ec.js new file mode 100644 index 0000000000..b4adb58d0f --- /dev/null +++ b/test/js/node/test/parallel/test-crypto-keygen-invalid-parameter-encoding-ec.js @@ -0,0 +1,27 @@ +'use strict'; + +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); + +const { + generateKeyPairSync, +} = require('crypto'); + +{ + assert.throws(() => generateKeyPairSync('ec', { + namedCurve: 'secp224r1', + publicKeyEncoding: { + format: 'jwk' + }, + privateKeyEncoding: { + format: 'jwk' + } + }), { + name: 'Error', + code: 'ERR_CRYPTO_JWK_UNSUPPORTED_CURVE', + message: 'Unsupported JWK EC curve: secp224r1.' + }); +} diff --git a/test/js/node/test/sequential/test-crypto-timing-safe-equal.js b/test/js/node/test/sequential/test-crypto-timing-safe-equal.js new file mode 100644 index 0000000000..c76b22444f --- /dev/null +++ b/test/js/node/test/sequential/test-crypto-timing-safe-equal.js @@ -0,0 +1,119 @@ +// Flags: --expose-internals --no-warnings --allow-natives-syntax +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const crypto = require('crypto'); + +// 'should consider equal strings to be equal' +assert.strictEqual( + crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('foo')), + true +); + +// 'should consider unequal strings to be unequal' +assert.strictEqual( + crypto.timingSafeEqual(Buffer.from('foo'), Buffer.from('bar')), + false +); + +{ + // Test TypedArrays with different lengths but equal byteLengths. + const buf = crypto.randomBytes(16).buffer; + const a1 = new Uint8Array(buf); + const a2 = new Uint16Array(buf); + const a3 = new Uint32Array(buf); + + for (const left of [a1, a2, a3]) { + for (const right of [a1, a2, a3]) { + assert.strictEqual(crypto.timingSafeEqual(left, right), true); + } + } +} + +{ + // When the inputs are floating-point numbers, timingSafeEqual neither has + // equality nor SameValue semantics. It just compares the underlying bytes, + // ignoring the TypedArray type completely. + + const cmp = (fn) => (a, b) => a.every((x, i) => fn(x, b[i])); + const eq = cmp((a, b) => a === b); + const is = cmp(Object.is); + + function test(a, b, { equal, sameValue, timingSafeEqual }) { + assert.strictEqual(eq(a, b), equal); + assert.strictEqual(is(a, b), sameValue); + assert.strictEqual(crypto.timingSafeEqual(a, b), timingSafeEqual); + } + + test(new Float32Array([NaN]), new Float32Array([NaN]), { + equal: false, + sameValue: true, + timingSafeEqual: true + }); + + test(new Float64Array([0]), new Float64Array([-0]), { + equal: true, + sameValue: false, + timingSafeEqual: false + }); + + const x = new BigInt64Array([0x7ff0000000000001n, 0xfff0000000000001n]); + test(new Float64Array(x.buffer), new Float64Array([NaN, NaN]), { + equal: false, + sameValue: true, + timingSafeEqual: false + }); +} + +assert.throws( + () => crypto.timingSafeEqual(Buffer.from([1, 2, 3]), Buffer.from([1, 2])), + { + code: 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', + name: 'RangeError', + message: 'Input buffers must have the same byte length' + } +); + +assert.throws( + () => crypto.timingSafeEqual('not a buffer', Buffer.from([1, 2])), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); + +assert.throws( + () => crypto.timingSafeEqual(Buffer.from([1, 2]), 'not a buffer'), + { + code: 'ERR_INVALID_ARG_TYPE', + name: 'TypeError', + } +); + +if (typeof Bun === 'undefined') { + // V8 Fast API + const foo = Buffer.from('foo'); + const bar = Buffer.from('bar'); + const longer = Buffer.from('longer'); + function testFastPath(buf1, buf2) { + return crypto.timingSafeEqual(buf1, buf2); + } + eval('%PrepareFunctionForOptimization(testFastPath)'); + assert.strictEqual(testFastPath(foo, bar), false); + eval('%OptimizeFunctionOnNextCall(testFastPath)'); + assert.strictEqual(testFastPath(foo, bar), false); + assert.strictEqual(testFastPath(foo, foo), true); + assert.throws(() => testFastPath(foo, longer), { + code: 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH', + }); + + if (common.isDebug) { + const { internalBinding } = require('internal/test/binding'); + const { getV8FastApiCallCount } = internalBinding('debug'); + assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.ok'), 2); + assert.strictEqual(getV8FastApiCallCount('crypto.timingSafeEqual.error'), 1); + } +} diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index f7a10a2bb8..1270c0a810 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -163,7 +163,7 @@ it("crypto.timingSafeEqual", () => { crypto.timingSafeEqual(uuid, uuid.slice(0, uuid.length - 2)); expect.unreachable(); } catch (e) { - expect(e.message).toBe("Input buffers must have the same length"); + expect(e.message).toBe("Input buffers must have the same byte length"); } try {