diff --git a/.claude/commands/upgrade-boringssl.md b/.claude/commands/upgrade-boringssl.md index b18ee4fc4051..5b9ef1ab6ed8 100644 --- a/.claude/commands/upgrade-boringssl.md +++ b/.claude/commands/upgrade-boringssl.md @@ -90,6 +90,7 @@ Then `bun run ci:watch` and fix anything that turns up. - **`EVP_PBE_validate_scrypt_params`** — `crypto/evp/scrypt.cc`, `include/openssl/evp.h` - **Electron `SSL_want` / `EVP_CIPHER_do_all_sorted`** — `ssl/ssl_lib.cc` (return `rwstate` directly), `ssl/ssl_test.cc` (drops the corresponding test block), `decrepit/evp/evp_do_all.cc`, `crypto/cipher/get_cipher.cc`, `include/openssl/cipher.h` - **MLDSA stack-frame pragma** — `crypto/fipsmodule/mldsa/mldsa.cc.inc` +- **PKCS#1 v1.5 signature prefixes for the digests above** — `crypto/fipsmodule/rsa/rsa.cc.inc` (`kPKCS1SigPrefixes` rows for SHA-512/224, SHA-512/256, SHA3-\* and RIPEMD-160), `crypto/rsa/rsa_test.cc` If upstream upstreams any of these (check `git grep` on `upstream/main` before re-applying), drop the fork's copy. diff --git a/scripts/build/deps/boringssl.ts b/scripts/build/deps/boringssl.ts index 9db3b212578d..ee422006e948 100644 --- a/scripts/build/deps/boringssl.ts +++ b/scripts/build/deps/boringssl.ts @@ -24,7 +24,7 @@ import { quote } from "../shell.ts"; import type { Dependency, DirectBuild } from "../source.ts"; import { LIBC_ALLOCATION_SYMBOLS, depSourceDir } from "../source.ts"; -const BORINGSSL_COMMIT = "41bf9b59c2ebf277a7aa427e1ecad5cc80dd4d4f"; +const BORINGSSL_COMMIT = "89a2ff2be775267283a2c8e35044e49ddab5e5aa"; export const boringssl: Dependency = { name: "boringssl", diff --git a/src/codegen/builtins/host/node-crypto-bindings.builtins.ts b/src/codegen/builtins/host/node-crypto-bindings.builtins.ts index b33970417aa8..f8c3d74298ec 100644 --- a/src/codegen/builtins/host/node-crypto-bindings.builtins.ts +++ b/src/codegen/builtins/host/node-crypto-bindings.builtins.ts @@ -12,6 +12,8 @@ export default b.declarations( "certExportPublicKey", "certExportChallenge", "getCurves", + "oneShotDigest", + "getHashes", "getCiphers", "getCipherInfo", "sign", diff --git a/src/codegen/builtins/host/runtime-dependencies.builtins.ts b/src/codegen/builtins/host/runtime-dependencies.builtins.ts index 291637f4a069..11a6ff05f5b1 100644 --- a/src/codegen/builtins/host/runtime-dependencies.builtins.ts +++ b/src/codegen/builtins/host/runtime-dependencies.builtins.ts @@ -601,12 +601,6 @@ const nativeCalls = [ onBehalfOf: environmentObjects, }, { file: inBindings("ProcessBindingUV.cpp"), caller: "jsErrname", onBehalfOf: ["process.binding"] }, - { - file: inBindings("node/crypto/CryptoGenRsaKeyPair.cpp"), - caller: "RsaKeyPairJobCtx::fromJS", - times: 2, - onBehalfOf: nodeCryptoBinding, - }, { file: inBindings("node/crypto/JSKeyObjectConstructor.cpp"), caller: "jsKeyObjectConstructor_from", diff --git a/src/js/internal/util/deprecate.ts b/src/js/internal/util/deprecate.ts index ed8d85aadc69..3fc0c5f0faa1 100644 --- a/src/js/internal/util/deprecate.ts +++ b/src/js/internal/util/deprecate.ts @@ -2,15 +2,15 @@ const { validateString } = require("internal/validators"); const codesWarned = new Set(); -function getDeprecationWarningEmitter( +function getDeprecationWarningEmitter( code: string | undefined, msg: string, - deprecated: (...args: unknown[]) => unknown, - shouldEmitWarning = () => true, + deprecated: ((...args: unknown[]) => unknown) | undefined, + shouldEmitWarning: (arg: Arg) => boolean = () => true, ) { let warned = false; - return function () { - if (!warned && shouldEmitWarning()) { + return function (arg: Arg) { + if (!warned && shouldEmitWarning(arg)) { warned = true; if (code !== undefined) { if (!codesWarned.has(code)) { @@ -68,4 +68,5 @@ function deprecate(fn: (this: unknown, ...args: unknown[]) => unknown, msg: stri export default { deprecate, + getDeprecationWarningEmitter, }; diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index eba2903f5208..f1376729a189 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -7,6 +7,8 @@ const { CryptoHasher } = Bun; const { getCurves, + getHashes, + oneShotDigest, certVerifySpkac, certExportPublicKey, certExportChallenge, @@ -76,7 +78,6 @@ const { getFips, setFips, setEngine, - getHashes, scrypt, scryptSync, argon2: _argon2, @@ -121,7 +122,9 @@ const { validateString, validateUint32, } = require("internal/validators"); -const { deprecate } = require("internal/util/deprecate"); +const { deprecate, getDeprecationWarningEmitter } = require("internal/util/deprecate"); + +const StringPrototypeToLowerCase = String.prototype.toLowerCase; const kHandle = Symbol("kHandle"); @@ -209,7 +212,9 @@ const crypto = globalThis.crypto; var crypto_exports: any = {}; -crypto_exports.getRandomValues = value => crypto.getRandomValues(value); +crypto_exports.getRandomValues = function getRandomValues(value) { + return crypto.getRandomValues(value); +}; crypto_exports.constants = $processBindingConstants.crypto; crypto_exports.KeyObject = KeyObject; @@ -227,8 +232,70 @@ crypto_exports.createPrivateKey = createPrivateKey; var webcrypto = crypto; var _subtle = webcrypto.subtle; -crypto_exports.hash = function hash(algorithm, input, outputEncoding: import("bun").DigestEncoding = "hex") { - return CryptoHasher.hash(algorithm, input, outputEncoding); +const maybeEmitDeprecationWarning = getDeprecationWarningEmitter( + "DEP0198", + "Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.", + undefined, + (algorithm: string) => { + if (algorithm.length !== 8 && algorithm.length !== 9) return false; + const name = StringPrototypeToLowerCase.$call(algorithm); + return name === "shake128" || name === "shake256" || name === "shake-128" || name === "shake-256"; + }, +); + +// https://github.com/nodejs/node/blob/v26.12.1/lib/internal/crypto/hash.js#L323-L391 +crypto_exports.hash = function hash(algorithm, input, options) { + validateString(algorithm, "algorithm"); + // oneShotDigest() tells the objects that it takes from the others. + if (typeof input !== "string" && !$isObject(input)) { + throw $ERR_INVALID_ARG_TYPE("input", ["Buffer", "TypedArray", "DataView", "string"], input); + } + if (options === undefined) { + maybeEmitDeprecationWarning(algorithm); + return oneShotDigest(algorithm, input, "hex"); + } + let outputEncoding; + let outputLength; + let hasDigestOptions = false; + + if (typeof options === "string") { + outputEncoding = options; + } else { + // Not in Node: the digest is written into a TypedArray or a DataView. + if (isArrayBufferView(options)) { + return CryptoHasher.hash(algorithm, input as Bun.BlobOrStringOrBuffer, options as NodeJS.TypedArray); + } + validateObject(options, "options"); + outputLength = options.outputLength; + outputEncoding = options.outputEncoding; + hasDigestOptions = options.functionName !== undefined || options.customization !== undefined; + } + + outputEncoding ??= "hex"; + + let normalized = outputEncoding; + // Fast case: if it's 'hex', we don't need to validate it further. + if (normalized !== "hex") { + validateString(outputEncoding, "outputEncoding"); + normalized = normalizeEncoding(outputEncoding); + // If the encoding is invalid, normalizeEncoding() returns undefined. + if (normalized === undefined) { + // normalizeEncoding() doesn't handle 'buffer'. + if (StringPrototypeToLowerCase.$call(outputEncoding) === "buffer") { + normalized = "buffer"; + } else { + throw $ERR_INVALID_ARG_VALUE("outputEncoding", outputEncoding); + } + } + } + + if (outputLength !== undefined) { + validateUint32(outputLength, "outputLength"); + } else { + maybeEmitDeprecationWarning(algorithm); + } + + return oneShotDigest(algorithm, input, normalized, outputLength, hasDigestOptions); }; crypto_exports.pbkdf2 = pbkdf2; @@ -312,7 +379,9 @@ function createVerify(algorithm: string, options?: $Node.WritableOptions): $Node crypto_exports.createVerify = createVerify; -function Hash(this: $Node.Hash, algorithm: string | $Node.NativeHash, options?: $Node.TransformOptions): void { +type HashOptions = $Node.TransformOptions & { outputLength?: number; functionName?: unknown; customization?: unknown }; + +function Hash(this: $Node.Hash, algorithm: string | $Node.NativeHash, options?: HashOptions): void { if (!new.target) { return new Hash(algorithm, options) as any; } @@ -320,6 +389,16 @@ function Hash(this: $Node.Hash, algorithm: string | $Node.NativeHash, options?: const handle = new _Hash(algorithm, options); this[kHandle] = handle; + if (typeof algorithm === "string") { + // They are for cSHAKE, which BoringSSL does not have. + if (options != null && (options.functionName !== undefined || options.customization !== undefined)) { + throw new Error("Digest method not supported"); + } + if (typeof options !== "object" || options === null || options.outputLength === undefined) { + maybeEmitDeprecationWarning(algorithm); + } + } + LazyTransform.$apply(this, [options]); } $toClass(Hash, "Hash", LazyTransform); diff --git a/src/jsc/bindings/JSX509Certificate.cpp b/src/jsc/bindings/JSX509Certificate.cpp index 07de22c7e62a..ed4ed6846cb2 100644 --- a/src/jsc/bindings/JSX509Certificate.cpp +++ b/src/jsc/bindings/JSX509Certificate.cpp @@ -136,20 +136,11 @@ static JSValue createX509Certificate(JSC::VM& vm, JSGlobalObject* globalObject, RETURN_IF_EXCEPTION(scope, {}); data = view->bytes(); } else if (auto* typedArray = dynamicDowncast(arg)) { - if (typedArray->isDetached()) [[unlikely]] { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "TypedArray is detached"_s); - return {}; - } data = typedArray->span(); } else if (auto* buffer = dynamicDowncast(arg)) { - auto* impl = buffer->impl(); - if (!impl) [[unlikely]] { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "Buffer is detached"_s); - return {}; - } - data = impl->span(); + data = buffer->impl()->span(); } else { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "X509Certificate constructor argument must be a Buffer, TypedArray, or string"_s); + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "buffer"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, arg); return {}; } @@ -170,16 +161,7 @@ JSC_DEFINE_HOST_FUNCTION(x509CertificateConstructorConstruct, (JSGlobalObject * VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); - if (!callFrame->argumentCount()) { - Bun::throwError(globalObject, scope, ErrorCode::ERR_MISSING_ARGS, "X509Certificate constructor requires at least one argument"_s); - return {}; - } - - JSValue arg = callFrame->uncheckedArgument(0); - if (!arg.isCell()) { - Bun::throwError(globalObject, scope, ErrorCode::ERR_INVALID_ARG_TYPE, "X509Certificate constructor argument must be a Buffer, TypedArray, or string"_s); - return {}; - } + JSValue arg = callFrame->argument(0); auto* zigGlobalObject = defaultGlobalObject(globalObject); Structure* structure = zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject); diff --git a/src/jsc/bindings/ncrypto.cpp b/src/jsc/bindings/ncrypto.cpp index d3f30afc1cd8..b95af0130a62 100644 --- a/src/jsc/bindings/ncrypto.cpp +++ b/src/jsc/bindings/ncrypto.cpp @@ -386,7 +386,8 @@ int BignumPointer::isPrime(int nchecks, }, &innerCb); } - return BN_is_prime_ex(get(), nchecks, ctx.get(), cb.get()); + // Not upstream: the candidate is the caller's, and fewer checks than this only suit one that BoringSSL drew at random. + return BN_is_prime_ex(get(), std::max(nchecks, BN_prime_checks_for_validation), ctx.get(), cb.get()); } bool BignumPointer::generate(const PrimeConfig& params, @@ -1172,10 +1173,23 @@ std::optional X509View::getFingerprint( return std::nullopt; } +namespace { +// Not upstream: OpenSSL's PEM reader skips a UTF-8 byte order mark at the start of its input, BoringSSL's does not. +constexpr Buffer SkipByteOrderMark( + const Buffer& pem) +{ + if (pem.len > 3 && pem.data[0] == 0xef && pem.data[1] == 0xbb && pem.data[2] == 0xbf) { + return { .data = pem.data + 3, .len = pem.len - 3 }; + } + return pem; +} +} // namespace + Result X509Pointer::Parse( Buffer buffer) { ClearErrorOnReturn clearErrorOnReturn; + buffer = SkipByteOrderMark(buffer); BIOPointer bio(BIO_new_mem_buf(buffer.data, buffer.len)); if (!bio) return Result(ERR_get_error()); @@ -1421,14 +1435,11 @@ DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey( if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) { return DHPointer::CheckPublicKeyResult::CHECK_FAILED; } -#ifndef OPENSSL_IS_BORINGSSL - // Boringssl does not define DH_CHECK_PUBKEY_TOO_SMALL or TOO_LARGE if (codes & DH_CHECK_PUBKEY_TOO_SMALL) { return DHPointer::CheckPublicKeyResult::TOO_SMALL; } else if (codes & DH_CHECK_PUBKEY_TOO_LARGE) { return DHPointer::CheckPublicKeyResult::TOO_LARGE; } -#endif if (codes != 0) { return DHPointer::CheckPublicKeyResult::INVALID; } @@ -1587,78 +1598,107 @@ const EVP_MD* getDigestByName(const WTF::StringView name) return digest; } +// Not upstream: OpenSSL compares the names of digests without regard to case, and knows more of them than +// EVP_get_digestbyname() in BoringSSL. These are the names in Node's getHashes(), in its order, for the digests +// that BoringSSL has, and md4, which Node leaves to the legacy provider. Node's tests take a name in getHashes() +// to work in pbkdf2() and hkdf() too, so SHAKE and BLAKE2s, which only createHash() and hash() have, are not here. +static constexpr DigestName digestNames[] = { + { "RSA-MD5"_s, EVP_md5 }, + { "RSA-RIPEMD160"_s, EVP_ripemd160 }, + { "RSA-SHA1"_s, EVP_sha1 }, + { "RSA-SHA1-2"_s, EVP_sha1 }, + { "RSA-SHA224"_s, EVP_sha224 }, + { "RSA-SHA256"_s, EVP_sha256 }, + { "RSA-SHA3-224"_s, EVP_sha3_224 }, + { "RSA-SHA3-256"_s, EVP_sha3_256 }, + { "RSA-SHA3-384"_s, EVP_sha3_384 }, + { "RSA-SHA3-512"_s, EVP_sha3_512 }, + { "RSA-SHA384"_s, EVP_sha384 }, + { "RSA-SHA512"_s, EVP_sha512 }, + { "RSA-SHA512/224"_s, EVP_sha512_224 }, + { "RSA-SHA512/256"_s, EVP_sha512_256 }, + { "blake2b-512"_s, EVP_blake2b512 }, + { "blake2b512"_s, EVP_blake2b512 }, + { "id-rsassa-pkcs1-v1_5-with-sha3-224"_s, EVP_sha3_224 }, + { "id-rsassa-pkcs1-v1_5-with-sha3-256"_s, EVP_sha3_256 }, + { "id-rsassa-pkcs1-v1_5-with-sha3-384"_s, EVP_sha3_384 }, + { "id-rsassa-pkcs1-v1_5-with-sha3-512"_s, EVP_sha3_512 }, + { "md4"_s, EVP_md4 }, + { "md5"_s, EVP_md5 }, + { "md5-sha1"_s, EVP_md5_sha1 }, + { "md5WithRSAEncryption"_s, EVP_md5 }, + { "ripemd"_s, EVP_ripemd160 }, + { "ripemd-160"_s, EVP_ripemd160 }, + { "ripemd160"_s, EVP_ripemd160 }, + { "ripemd160WithRSA"_s, EVP_ripemd160 }, + { "rmd160"_s, EVP_ripemd160 }, + { "sha-1"_s, EVP_sha1 }, + { "sha-224"_s, EVP_sha224 }, + { "sha-256"_s, EVP_sha256 }, + { "sha-384"_s, EVP_sha384 }, + { "sha-512"_s, EVP_sha512 }, + { "sha-512/224"_s, EVP_sha512_224 }, + { "sha-512/256"_s, EVP_sha512_256 }, + { "sha1"_s, EVP_sha1 }, + { "sha1WithRSAEncryption"_s, EVP_sha1 }, + { "sha2-224"_s, EVP_sha224 }, + { "sha2-256"_s, EVP_sha256 }, + { "sha2-384"_s, EVP_sha384 }, + { "sha2-512"_s, EVP_sha512 }, + { "sha2-512/224"_s, EVP_sha512_224 }, + { "sha2-512/256"_s, EVP_sha512_256 }, + { "sha224"_s, EVP_sha224 }, + { "sha224WithRSAEncryption"_s, EVP_sha224 }, + { "sha256"_s, EVP_sha256 }, + { "sha256WithRSAEncryption"_s, EVP_sha256 }, + { "sha3-224"_s, EVP_sha3_224 }, + { "sha3-256"_s, EVP_sha3_256 }, + { "sha3-384"_s, EVP_sha3_384 }, + { "sha3-512"_s, EVP_sha3_512 }, + { "sha384"_s, EVP_sha384 }, + { "sha384WithRSAEncryption"_s, EVP_sha384 }, + { "sha512"_s, EVP_sha512 }, + { "sha512-224"_s, EVP_sha512_224 }, + { "sha512-224WithRSAEncryption"_s, EVP_sha512_224 }, + { "sha512-256"_s, EVP_sha512_256 }, + { "sha512-256WithRSAEncryption"_s, EVP_sha512_256 }, + { "sha512WithRSAEncryption"_s, EVP_sha512 }, + { "ssl3-md5"_s, EVP_md5 }, + { "ssl3-sha1"_s, EVP_sha1 }, +}; + +std::span getDigestNames() +{ + return digestNames; +} + static const EVP_MD* findDigestByName(const WTF::StringView name) { + for (const auto& [candidate, digest] : digestNames) { + if (WTF::equalIgnoringASCIICase(name, candidate)) { + return digest(); + } + } + // Historically, "dss1" and "DSS1" were DSA aliases for SHA-1 // exposed through the public API. if (name == "dss1"_s || name == "DSS1"_s) [[unlikely]] { return EVP_sha1(); } - if (WTF::equalIgnoringASCIICase(name, "md5"_s)) { - return EVP_md5(); - } - - if (WTF::startsWithIgnoringASCIICase(name, "rsa-sha"_s)) { - auto bits = name.substring(7); - if (WTF::equalIgnoringASCIICase(bits, "1"_s)) { - return EVP_sha1(); - } - if (WTF::equalIgnoringASCIICase(bits, "224"_s)) { - return EVP_sha224(); - } - if (WTF::equalIgnoringASCIICase(bits, "256"_s)) { - return EVP_sha256(); - } - if (WTF::equalIgnoringASCIICase(bits, "384"_s)) { - return EVP_sha384(); - } - if (WTF::equalIgnoringASCIICase(bits, "512"_s)) { - return EVP_sha512(); - } - } - - if (WTF::startsWithIgnoringASCIICase(name, "sha"_s)) { - auto remain = name.substring(3); - if (remain.startsWith('-')) { - auto bits = remain.substring(1); - if (WTF::equalIgnoringASCIICase(bits, "1"_s)) { - return EVP_sha1(); - } - if (WTF::equalIgnoringASCIICase(bits, "224"_s)) { - return EVP_sha224(); - } - if (WTF::equalIgnoringASCIICase(bits, "256"_s)) { - return EVP_sha256(); - } - if (WTF::equalIgnoringASCIICase(bits, "384"_s)) { - return EVP_sha384(); - } - - if (WTF::startsWithIgnoringASCIICase(bits, "512"_s)) { - auto moreBits = bits.substring(3); - if (moreBits.isEmpty()) { - return EVP_sha512(); - } - if (WTF::equalIgnoringASCIICase(moreBits, "/224"_s)) { - return EVP_sha512_224(); - } - if (WTF::equalIgnoringASCIICase(moreBits, "/256"_s)) { - return EVP_sha512_256(); - } - - // backwards compatibility with what we supported before - // (not supported by node) - if (WTF::equalIgnoringASCIICase(moreBits, "256"_s) || WTF::equalIgnoringASCIICase(moreBits, "_256"_s)) { - return EVP_sha512_256(); - } - } - } - - // backwards compatibility with what we supported before - // (not supported by node) - if (WTF::equalIgnoringASCIICase(remain, "128"_s)) { - return EVP_sha1(); + // backwards compatibility with what we supported before + // (not supported by node) + static constexpr DigestName otherNames[] = { + { "blake2b256"_s, EVP_blake2b256 }, + { "sha-512224"_s, EVP_sha512_224 }, + { "sha-512256"_s, EVP_sha512_256 }, + { "sha-512_224"_s, EVP_sha512_224 }, + { "sha-512_256"_s, EVP_sha512_256 }, + { "sha128"_s, EVP_sha1 }, + }; + for (const auto& [candidate, digest] : otherNames) { + if (WTF::equalIgnoringASCIICase(name, candidate)) { + return digest(); } } @@ -2161,7 +2201,8 @@ bool EVPKeyPointer::IsRSAPrivateKey(const Buffer& buffer) EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM( const Buffer& buffer) { - auto bp = BIOPointer::New(buffer.data, buffer.len); + auto pem = SkipByteOrderMark(buffer); + auto bp = BIOPointer::New(pem.data, pem.len); if (!bp) return ParseKeyResult(PKParseError::FAILED); // Try parsing as SubjectPublicKeyInfo (SPKI) first. @@ -2266,7 +2307,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( return ParseKeyResult(WTF::move(pkey)); }; - auto bio = BIOPointer::New(buffer); + auto bio = BIOPointer::New(config.format == PKFormatType::PEM ? SkipByteOrderMark(buffer) : buffer); if (!bio) return ParseKeyResult(PKParseError::FAILED); auto passphrase = GetPassphrase(config); @@ -2306,6 +2347,13 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey( return keyOrError(EVPKeyPointer(EVP_PKCS82PKEY(p8inf.get()))); } case PKEncodingType::SEC1: { + // Not upstream: BoringSSL's d2i_PrivateKey_bio() tells the kind of key by the number of + // elements, and takes an ECPrivateKey without its optional publicKey for an RSA key. + const unsigned char* p = buffer.data; + if (EVPKeyPointer key { d2i_PrivateKey(EVP_PKEY_EC, nullptr, &p, buffer.len) }) { + return keyOrError(WTF::move(key)); + } + ERR_clear_error(); auto key = d2i_PrivateKey_bio(bio.get(), nullptr); return keyOrError(EVPKeyPointer(key)); } @@ -2603,6 +2651,8 @@ const Cipher Cipher::FromName(WTF::StringView name) if (remain == "256"_s) return Cipher::AES_256_CBC(); } + if (WTF::equalIgnoringASCIICase(name, "des3"_s)) return Cipher::FromNid(NID_des_ede3_cbc); + return Cipher(getCipherByName(name)); } diff --git a/src/jsc/bindings/ncrypto.h b/src/jsc/bindings/ncrypto.h index 8ed9d29bca66..d830cb08c08c 100644 --- a/src/jsc/bindings/ncrypto.h +++ b/src/jsc/bindings/ncrypto.h @@ -903,14 +903,9 @@ class DHPointer final { enum class CheckPublicKeyResult { NONE, -#ifndef OPENSSL_IS_BORINGSSL - // Boringssl does not define DH_R_CHECK_PUBKEY_TOO_SMALL or TOO_LARGE - TOO_SMALL = DH_R_CHECK_PUBKEY_TOO_SMALL, - TOO_LARGE = DH_R_CHECK_PUBKEY_TOO_LARGE, - INVALID = DH_R_CHECK_PUBKEY_INVALID, -#else - INVALID = DH_R_INVALID_PUBKEY, -#endif + TOO_SMALL, + TOO_LARGE, + INVALID, CHECK_FAILED = 512, }; // Check to see if the given public key is suitable for this DH instance. @@ -1265,6 +1260,14 @@ Buffer ExportChallenge(const char* input, size_t length); const EVP_MD* getDigestByName(const WTF::StringView name); const EVP_CIPHER* getCipherByName(const WTF::StringView name); +// Not upstream. +struct DigestName { + WTF::ASCIILiteral name; + const EVP_MD* (*digest)(); +}; +// The names in getHashes(). getDigestByName() looks through them first. +std::span getDigestNames(); + // Verify that the specified HKDF output length is valid for the given digest. // The maximum length for HKDF output for a given digest is 255 times the // hash size for the given digest algorithm. diff --git a/src/jsc/bindings/node/crypto/CryptoDhJob.cpp b/src/jsc/bindings/node/crypto/CryptoDhJob.cpp index 86258f12fe24..9a263353e95c 100644 --- a/src/jsc/bindings/node/crypto/CryptoDhJob.cpp +++ b/src/jsc/bindings/node/crypto/CryptoDhJob.cpp @@ -68,6 +68,21 @@ void DhJob::createAndSchedule(JSGlobalObject* globalObject, DhJobCtx&& ctx, JSVa Bun__DhJob__createAndSchedule(globalObject, ctxCopy, JSValue::encode(callback)); } +static KeyObject keyFromOption(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, CryptoKeyType type, ASCIILiteral name) +{ + auto prepared = type == CryptoKeyType::Private + ? KeyObject::preparePrivateKey(globalObject, scope, value, name) + : KeyObject::preparePublicOrPrivateKey(globalObject, scope, value, name); + RETURN_IF_EXCEPTION(scope, {}); + + if (prepared.keyData) { + return KeyObject::create(type, WTF::move(*prepared.keyData)); + } + + RELEASE_AND_RETURN(scope, KeyObject::getPublicOrPrivateKey(globalObject, scope, prepared.keyDataView, type, prepared.formatType, prepared.encodingType, prepared.cipher, WTF::move(prepared.passphrase))); +} + +// https://github.com/nodejs/node/blob/v26.12.1/lib/internal/crypto/diffiehellman.js#L289-L343 std::optional DhJobCtx::fromJS(JSGlobalObject* globalObject, ThrowScope& scope, JSC::JSObject* options) { VM& vm = globalObject->vm(); @@ -77,44 +92,52 @@ std::optional DhJobCtx::fromJS(JSGlobalObject* globalObject, ThrowScop JSValue publicKeyValue = options->get(globalObject, Identifier::fromString(vm, "publicKey"_s)); RETURN_IF_EXCEPTION(scope, std::nullopt); - JSKeyObject* privateKeyObject = dynamicDowncast(privateKeyValue); - if (!privateKeyObject) { + if (privateKeyValue.isUndefinedOrNull()) { ERR::INVALID_ARG_VALUE(scope, globalObject, "options.privateKey"_s, privateKeyValue); return std::nullopt; } - JSKeyObject* publicKeyObject = dynamicDowncast(publicKeyValue); - if (!publicKeyObject) { + if (publicKeyValue.isUndefinedOrNull()) { ERR::INVALID_ARG_VALUE(scope, globalObject, "options.publicKey"_s, publicKeyValue); return std::nullopt; } - const KeyObject& privateKey = privateKeyObject->handle(); - const KeyObject& publicKey = publicKeyObject->handle(); - - if (privateKey.type() != CryptoKeyType::Private) { - ERR::CRYPTO_INVALID_KEY_OBJECT_TYPE(scope, globalObject, privateKey.type(), "private"_s); + JSKeyObject* privateKeyObject = dynamicDowncast(privateKeyValue); + if (privateKeyObject && privateKeyObject->handle().type() != CryptoKeyType::Private) { + ERR::CRYPTO_INVALID_KEY_OBJECT_TYPE(scope, globalObject, privateKeyObject->handle().type(), "private"_s); return std::nullopt; } - if (publicKey.type() != CryptoKeyType::Public && publicKey.type() != CryptoKeyType::Private) { - ERR::CRYPTO_INVALID_KEY_OBJECT_TYPE(scope, globalObject, publicKey.type(), "private or public"_s); + JSKeyObject* publicKeyObject = dynamicDowncast(publicKeyValue); + if (publicKeyObject && publicKeyObject->handle().type() == CryptoKeyType::Secret) { + ERR::CRYPTO_INVALID_KEY_OBJECT_TYPE(scope, globalObject, publicKeyObject->handle().type(), "private or public"_s); return std::nullopt; } - static constexpr auto supportedKeyTypes = std::to_array({ EVP_PKEY_DH, - EVP_PKEY_EC, - EVP_PKEY_X448, - EVP_PKEY_X25519 }); - - int privateKeyType = privateKey.asymmetricKey().id(); - int publicKeyType = publicKey.asymmetricKey().id(); - - if (privateKeyType != publicKeyType || std::ranges::find(supportedKeyTypes, privateKeyType) == supportedKeyTypes.end()) { - ERR::INVALID_ARG_VALUE(scope, globalObject, "options.privateKey"_s, privateKeyValue, "must be a supported key type"_s); - return std::nullopt; + if (privateKeyObject && publicKeyObject) { + static constexpr auto supportedKeyTypes = std::to_array({ EVP_PKEY_DH, + EVP_PKEY_EC, + EVP_PKEY_X448, + EVP_PKEY_X25519 }); + + int privateKeyType = privateKeyObject->handle().asymmetricKey().id(); + int publicKeyType = publicKeyObject->handle().asymmetricKey().id(); + + if (privateKeyType != publicKeyType || std::ranges::find(supportedKeyTypes, privateKeyType) == supportedKeyTypes.end()) { + auto privateName = privateKeyObject->handle().asymmetricKeyType(globalObject).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + auto publicName = publicKeyObject->handle().asymmetricKeyType(globalObject).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INCOMPATIBLE_KEY, makeString("Incompatible key types for Diffie-Hellman: "_s, privateName, " and "_s, publicName)); + return std::nullopt; + } } + KeyObject publicKey = keyFromOption(globalObject, scope, publicKeyValue, CryptoKeyType::Public, "options.publicKey"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + KeyObject privateKey = keyFromOption(globalObject, scope, privateKeyValue, CryptoKeyType::Private, "options.privateKey"_s); + RETURN_IF_EXCEPTION(scope, std::nullopt); + return DhJobCtx(privateKey.data(), publicKey.data()); } diff --git a/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp b/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp index 853d43cc5904..b4f614fa3d39 100644 --- a/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp +++ b/src/jsc/bindings/node/crypto/CryptoGenRsaKeyPair.cpp @@ -2,7 +2,6 @@ #include "ErrorCode.h" #include "NodeValidator.h" #include "CryptoUtil.h" -#include "BunProcess.h" using namespace Bun; using namespace JSC; @@ -121,10 +120,7 @@ __attribute__((minsize)) std::optional RsaKeyPairJobCtx::fromJ encodingConfig); } - JSValue hashValue = optionsValue.get(globalObject, Identifier::fromString(vm, "hash"_s)); - RETURN_IF_EXCEPTION(scope, std::nullopt); - JSValue mgf1HashValue = optionsValue.get(globalObject, Identifier::fromString(vm, "mgf1Hash"_s)); - RETURN_IF_EXCEPTION(scope, std::nullopt); + // https://github.com/nodejs/node/blob/v26.12.1/lib/internal/crypto/keygen.js#L197-L230 JSValue hashAlgorithmValue = optionsValue.get(globalObject, Identifier::fromString(vm, "hashAlgorithm"_s)); RETURN_IF_EXCEPTION(scope, std::nullopt); JSValue mgf1HashAlgorithmValue = optionsValue.get(globalObject, Identifier::fromString(vm, "mgf1HashAlgorithm"_s)); @@ -133,13 +129,9 @@ __attribute__((minsize)) std::optional RsaKeyPairJobCtx::fromJ RETURN_IF_EXCEPTION(scope, std::nullopt); JSString* hashString = nullptr; - GCOwnedDataScope hashView(hashString, WTF::nullStringView()); - JSString* hashAlgorithmString = nullptr; - GCOwnedDataScope hashAlgorithmView(hashAlgorithmString, WTF::nullStringView()); + GCOwnedDataScope hash(hashString, WTF::nullStringView()); JSString* mgf1HashString = nullptr; - GCOwnedDataScope mgf1HashView(mgf1HashString, WTF::nullStringView()); - JSString* mgf1HashAlgorithmString = nullptr; - GCOwnedDataScope mgf1HashAlgorithmView(mgf1HashAlgorithmString, WTF::nullStringView()); + GCOwnedDataScope mgf1Hash(mgf1HashString, WTF::nullStringView()); std::optional saltLength = std::nullopt; if (!saltLengthValue.isUndefined()) { @@ -153,48 +145,25 @@ __attribute__((minsize)) std::optional RsaKeyPairJobCtx::fromJ RETURN_IF_EXCEPTION(scope, std::nullopt); hashString = hashAlgorithmValue.toString(globalObject); RETURN_IF_EXCEPTION(scope, std::nullopt); - hashView = hashString->view(globalObject); + hash = hashString->view(globalObject); RETURN_IF_EXCEPTION(scope, std::nullopt); } if (!mgf1HashAlgorithmValue.isUndefined()) { V::validateString(scope, globalObject, mgf1HashAlgorithmValue, "options.mgf1HashAlgorithm"_s); RETURN_IF_EXCEPTION(scope, std::nullopt); - mgf1HashAlgorithmString = mgf1HashAlgorithmValue.toString(globalObject); + mgf1HashString = mgf1HashAlgorithmValue.toString(globalObject); RETURN_IF_EXCEPTION(scope, std::nullopt); - mgf1HashView = mgf1HashAlgorithmString->view(globalObject); + mgf1Hash = mgf1HashString->view(globalObject); RETURN_IF_EXCEPTION(scope, std::nullopt); } - if (!hashValue.isUndefined()) { - Bun::Process::emitWarning(globalObject, jsString(vm, makeString("\"options.hash\" is deprecated, use \"options.hashAlgorithm\" instead."_s)), jsString(vm, makeString("DeprecationWarning"_s)), jsString(vm, makeString("DEP0154"_s)), jsUndefined()); - RETURN_IF_EXCEPTION(scope, std::nullopt); - V::validateString(scope, globalObject, hashValue, "options.hash"_s); - RETURN_IF_EXCEPTION(scope, std::nullopt); - hashString = hashValue.toString(globalObject); + for (auto name : { "hash"_s, "mgf1Hash"_s }) { + JSValue removed = optionsValue.get(globalObject, Identifier::fromString(vm, name)); RETURN_IF_EXCEPTION(scope, std::nullopt); - hashView = hashString->view(globalObject); - RETURN_IF_EXCEPTION(scope, std::nullopt); - if (!hashAlgorithmView->isNull() && hashAlgorithmView != hashView) { - ERR::INVALID_ARG_VALUE(scope, globalObject, "options.hash"_s, hashValue); + if (!removed.isUndefined()) { + ERR::INVALID_ARG_VALUE(scope, globalObject, makeString("options."_s, name), removed, "is no longer supported"_s); return std::nullopt; } } - if (!mgf1HashValue.isUndefined()) { - Bun::Process::emitWarning(globalObject, jsString(vm, makeString("\"options.mgf1Hash\" is deprecated, use \"options.mgf1HashAlgorithm\" instead."_s)), jsString(vm, makeString("DeprecationWarning"_s)), jsString(vm, makeString("DEP0154"_s)), jsUndefined()); - RETURN_IF_EXCEPTION(scope, std::nullopt); - V::validateString(scope, globalObject, mgf1HashValue, "options.mgf1Hash"_s); - RETURN_IF_EXCEPTION(scope, std::nullopt); - mgf1HashString = mgf1HashValue.toString(globalObject); - RETURN_IF_EXCEPTION(scope, std::nullopt); - mgf1HashView = mgf1HashString->view(globalObject); - RETURN_IF_EXCEPTION(scope, std::nullopt); - if (!mgf1HashAlgorithmView->isNull() && mgf1HashAlgorithmView != mgf1HashView) { - ERR::INVALID_ARG_VALUE(scope, globalObject, "options.mgf1Hash"_s, mgf1HashValue); - return std::nullopt; - } - } - - GCOwnedDataScope hash = hashAlgorithmView->isNull() ? hashView : hashAlgorithmView; - GCOwnedDataScope mgf1Hash = mgf1HashAlgorithmView->isNull() ? mgf1HashView : mgf1HashAlgorithmView; ncrypto::Digest md = nullptr; ncrypto::Digest mgf1Md = nullptr; diff --git a/src/jsc/bindings/node/crypto/CryptoPrimes.cpp b/src/jsc/bindings/node/crypto/CryptoPrimes.cpp index 47b2e72b32e5..b1a7187c986a 100644 --- a/src/jsc/bindings/node/crypto/CryptoPrimes.cpp +++ b/src/jsc/bindings/node/crypto/CryptoPrimes.cpp @@ -81,6 +81,10 @@ JSC_DEFINE_HOST_FUNCTION(jsCheckPrimeSync, (JSC::JSGlobalObject * lexicalGlobalO RETURN_IF_EXCEPTION(scope, {}); } + if (!candidateValue.inherits() && !candidateValue.inherits()) { + return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "candidate"_s, "bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"_s, candidateValue); + } + auto candidateView = getArrayBufferOrView2(lexicalGlobalObject, scope, candidateValue, "candidate"_s, jsUndefined()); RETURN_IF_EXCEPTION(scope, {}); @@ -127,6 +131,10 @@ JSC_DEFINE_HOST_FUNCTION(jsCheckPrime, (JSC::JSGlobalObject * lexicalGlobalObjec RETURN_IF_EXCEPTION(scope, {}); } + if (!candidateValue.inherits() && !candidateValue.inherits()) { + return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "candidate"_s, "bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"_s, candidateValue); + } + auto candidateView = getArrayBufferOrView2(lexicalGlobalObject, scope, candidateValue, "candidate"_s, jsUndefined()); RETURN_IF_EXCEPTION(scope, {}); @@ -278,6 +286,31 @@ JSValue GeneratePrimeJob::result(JSGlobalObject* globalObject, JSC::ThrowScope& return JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(), WTF::move(buf)); } +// `options.add` or `options.rem` of generatePrime(). Empty when the option is not there. +static ncrypto::BignumPointer bignumOption(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, ASCIILiteral name) +{ + if (value.isUndefined()) { + return {}; + } + + if (value.isBigInt()) { + value = unsignedBigIntToBuffer(globalObject, scope, value, name); + RETURN_IF_EXCEPTION(scope, {}); + } + + auto bytes = getBuffer(value); + if (!bytes) { + ERR::INVALID_ARG_TYPE(scope, globalObject, name, "bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView"_s, value); + return {}; + } + + ncrypto::BignumPointer number(bytes->data(), bytes->size()); + if (!number) { + ERR::CRYPTO_OPERATION_FAILED(scope, globalObject, "could not generate prime"_s); + } + return number; +} + JSC_DEFINE_HOST_FUNCTION(jsGeneratePrime, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { auto& vm = lexicalGlobalObject->vm(); @@ -333,37 +366,11 @@ JSC_DEFINE_HOST_FUNCTION(jsGeneratePrime, (JSC::JSGlobalObject * lexicalGlobalOb ncrypto::ClearErrorOnReturn clear; - ncrypto::BignumPointer add; - if (!addValue.isUndefined()) { - if (addValue.isBigInt()) { - addValue = unsignedBigIntToBuffer(lexicalGlobalObject, scope, addValue, "options.add"_s); - RETURN_IF_EXCEPTION(scope, {}); - } - auto* addView = dynamicDowncast(addValue); - if (!addView) { - return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "options.add"_s, "ArrayBuffer, Buffer, TypedArray, DataView, or bigint"_s, addValue); - } - add.reset(reinterpret_cast(addView->vector()), addView->byteLength()); - if (!add) { - return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "could not generate prime"_s); - } - } + ncrypto::BignumPointer add = bignumOption(lexicalGlobalObject, scope, addValue, "options.add"_s); + RETURN_IF_EXCEPTION(scope, {}); - ncrypto::BignumPointer rem; - if (!remValue.isUndefined()) { - if (remValue.isBigInt()) { - remValue = unsignedBigIntToBuffer(lexicalGlobalObject, scope, remValue, "options.rem"_s); - RETURN_IF_EXCEPTION(scope, {}); - } - auto* remView = dynamicDowncast(remValue); - if (!remView) { - return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "options.rem"_s, "ArrayBuffer, Buffer, TypedArray, DataView, or bigint"_s, remValue); - } - rem.reset(reinterpret_cast(remView->vector()), remView->byteLength()); - if (!rem) { - return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "could not generate prime"_s); - } - } + ncrypto::BignumPointer rem = bignumOption(lexicalGlobalObject, scope, remValue, "options.rem"_s); + RETURN_IF_EXCEPTION(scope, {}); if (add) { if (ncrypto::BignumPointer::GetBitCount(add.get()) > size) [[unlikely]] { @@ -434,37 +441,11 @@ JSC_DEFINE_HOST_FUNCTION(jsGeneratePrimeSync, (JSC::JSGlobalObject * lexicalGlob ncrypto::ClearErrorOnReturn clear; - ncrypto::BignumPointer add; - if (!addValue.isUndefined()) { - if (addValue.isBigInt()) { - addValue = unsignedBigIntToBuffer(lexicalGlobalObject, scope, addValue, "options.add"_s); - RETURN_IF_EXCEPTION(scope, {}); - } - auto* addView = dynamicDowncast(addValue); - if (!addView) { - return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "options.add"_s, "ArrayBuffer, Buffer, TypedArray, DataView, or bigint"_s, addValue); - } - add.reset(reinterpret_cast(addView->vector()), addView->byteLength()); - if (!add) { - return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "could not generate prime"_s); - } - } + ncrypto::BignumPointer add = bignumOption(lexicalGlobalObject, scope, addValue, "options.add"_s); + RETURN_IF_EXCEPTION(scope, {}); - ncrypto::BignumPointer rem; - if (!remValue.isUndefined()) { - if (remValue.isBigInt()) { - remValue = unsignedBigIntToBuffer(lexicalGlobalObject, scope, remValue, "options.rem"_s); - RETURN_IF_EXCEPTION(scope, {}); - } - auto* remView = dynamicDowncast(remValue); - if (!remView) { - return ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "options.rem"_s, "ArrayBuffer, Buffer, TypedArray, DataView, or bigint"_s, remValue); - } - rem.reset(reinterpret_cast(remView->vector()), remView->byteLength()); - if (!rem) { - return ERR::CRYPTO_OPERATION_FAILED(scope, lexicalGlobalObject, "could not generate prime"_s); - } - } + ncrypto::BignumPointer rem = bignumOption(lexicalGlobalObject, scope, remValue, "options.rem"_s); + RETURN_IF_EXCEPTION(scope, {}); if (add) { if (ncrypto::BignumPointer::GetBitCount(add.get()) > size) [[unlikely]] { diff --git a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp index 61995f15e262..f8c4f77cc228 100644 --- a/src/jsc/bindings/node/crypto/CryptoSignJob.cpp +++ b/src/jsc/bindings/node/crypto/CryptoSignJob.cpp @@ -321,7 +321,7 @@ std::optional SignJobCtx::fromJS(JSGlobalObject* globalObject, Throw Vector signatureData; if (mode == Mode::Verify) { - auto signatureView = getArrayBufferOrView2(globalObject, scope, signatureValue, "signature"_s, jsUndefined(), true); + auto signatureView = getArrayBufferOrView2(globalObject, scope, signatureValue, "signature"_s, jsUndefined()); RETURN_IF_EXCEPTION(scope, {}); auto copy = copyArgumentBytes(globalObject, scope, std::span { signatureView->data(), signatureView->size() }, "signature"_s); if (!copy) return std::nullopt; diff --git a/src/jsc/bindings/node/crypto/CryptoUtil.cpp b/src/jsc/bindings/node/crypto/CryptoUtil.cpp index d9eee1b67fd4..fecea00f0d05 100644 --- a/src/jsc/bindings/node/crypto/CryptoUtil.cpp +++ b/src/jsc/bindings/node/crypto/CryptoUtil.cpp @@ -199,7 +199,7 @@ JSValue unsignedBigIntToBuffer(JSGlobalObject* lexicalGlobalObject, ThrowScope& WebCore::BufferEncodingType getEncodingDefaultBuffer(JSGlobalObject* globalObject, ThrowScope& scope, JSValue encodingValue) { BufferEncodingType res = BufferEncodingType::buffer; - if (encodingValue.isUndefinedOrNull() || !encodingValue.isString()) { + if (!encodingValue.toBoolean(globalObject)) { return res; } @@ -210,7 +210,13 @@ WebCore::BufferEncodingType getEncodingDefaultBuffer(JSGlobalObject* globalObjec return res; } - return parseEnumerationFromView(encodingString).value_or(BufferEncodingType::buffer); + auto encoding = parseEnumerationFromView(encodingString); + if (!encoding) { + ERR::UNKNOWN_ENCODING(scope, globalObject, encodingValue); + return res; + } + + return *encoding; } ncrypto::EVPKeyPointer::PKFormatType parseKeyFormat(JSC::JSGlobalObject* globalObject, JSValue formatValue, WTF::ASCIILiteral optionName, std::optional defaultFormat) @@ -478,7 +484,7 @@ std::optional getSaltLength(JSC::JSGlobalObject* globalObject, JSC::Thr DSASigEnc getDSASigEnc(JSC::JSGlobalObject* globalObject, ThrowScope& scope, JSValue options) { - if (!options.isObject() || (options.asCell()->type() != JSC::JSType::FinalObjectType && options.asCell()->type() != JSC::JSType::CompiledObjectType)) { + if (!options.isObject() || options.isCallable()) { return DSASigEnc::DER; } @@ -592,8 +598,28 @@ JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, Throw return getArrayBufferOrView(globalObject, scope, value, argName, jsUndefined()); } +// What Node's getArrayBufferOrView() makes Buffer.from() read a string with. Empty after throwing. +std::optional getInputEncoding(JSGlobalObject* globalObject, ThrowScope& scope, JSValue encodingValue) +{ + if (!encodingValue.isString()) { + return BufferEncodingType::utf8; + } + + auto encodingView = asString(encodingValue)->view(globalObject); + RETURN_IF_EXCEPTION(scope, std::nullopt); + if (encodingView->isEmpty() || encodingView == "buffer"_s) { + return BufferEncodingType::utf8; + } + + auto encoding = parseEnumerationFromView(encodingView); + if (!encoding) { + ERR::UNKNOWN_ENCODING(scope, globalObject, encodingValue); + } + return encoding; +} + // maybe replace other getArrayBufferOrView -GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* globalObject, ThrowScope& scope, JSValue dataValue, ASCIILiteral argName, JSValue encodingValue, bool arrayBufferViewOnly) +GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* globalObject, ThrowScope& scope, JSValue dataValue, ASCIILiteral argName, JSValue encodingValue) { using Return = GCOwnedDataScope>; @@ -601,11 +627,6 @@ GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* return { view, view->span() }; } - if (arrayBufferViewOnly) { - ERR::INVALID_ARG_INSTANCE(scope, globalObject, argName, "Buffer, TypedArray, or DataView"_s, dataValue); - return { nullptr, {} }; - }; - if (auto* arrayBuffer = dynamicDowncast(dataValue)) { return { arrayBuffer, arrayBuffer->impl()->span() }; } @@ -616,19 +637,10 @@ GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* auto strView = str->view(globalObject); RETURN_IF_EXCEPTION(scope, Return(nullptr, {})); - BufferEncodingType encoding = BufferEncodingType::utf8; - if (encodingValue.isString()) { - auto* encodingString = encodingValue.toString(globalObject); - RETURN_IF_EXCEPTION(scope, Return(nullptr, {})); - auto encodingView = encodingString->view(globalObject); - RETURN_IF_EXCEPTION(scope, Return(nullptr, {})); - - if (encodingView != "buffer"_s) { - encoding = parseEnumerationFromView(encodingView).value_or(BufferEncodingType::utf8); - } - } + auto encoding = getInputEncoding(globalObject, scope, encodingValue); + RETURN_IF_EXCEPTION(scope, Return(nullptr, {})); - JSValue buffer = JSValue::decode(WebCore::constructFromEncoding(globalObject, strView, encoding)); + JSValue buffer = JSValue::decode(WebCore::constructFromEncoding(globalObject, strView, *encoding)); RETURN_IF_EXCEPTION(scope, Return(nullptr, {})); if (auto* view = dynamicDowncast(buffer)) { @@ -646,19 +658,16 @@ JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, Throw JSString* dataString = value.toString(globalObject); RETURN_IF_EXCEPTION(scope, {}); - auto maybeEncoding = encodingValue.pureToBoolean() == TriState::True ? WebCore::parseEnumerationAllowBuffer(*globalObject, encodingValue) : std::optional { BufferEncodingType::utf8 }; - RETURN_IF_EXCEPTION(scope, {}); - - if (!maybeEncoding && !defaultBufferEncoding) { - ERR::UNKNOWN_ENCODING(scope, globalObject, encodingValue); - return {}; - } - - auto encoding = maybeEncoding.has_value() ? maybeEncoding.value() : BufferEncodingType::buffer; - - if (encoding == BufferEncodingType::hex && dataString->length() % 2 != 0) { - Bun::ERR::INVALID_ARG_VALUE(scope, globalObject, "encoding"_s, encodingValue, makeString("is invalid for data of length "_s, dataString->length())); - return {}; + auto encoding = BufferEncodingType::utf8; + if (defaultBufferEncoding) { + if (encodingValue.isString()) { + encoding = WebCore::parseEnumerationAllowBuffer(*globalObject, encodingValue).value_or(BufferEncodingType::buffer); + RETURN_IF_EXCEPTION(scope, {}); + } + } else { + auto inputEncoding = getInputEncoding(globalObject, scope, encodingValue); + RETURN_IF_EXCEPTION(scope, {}); + encoding = *inputEncoding; } auto dataView = dataString->view(globalObject); @@ -682,9 +691,21 @@ JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, Throw return view; } + if (auto* arrayBuffer = dynamicDowncast(value)) { + RefPtr impl = arrayBuffer->impl(); + if (impl->isDetached()) { + throwTypeError(globalObject, scope, "Buffer is detached"_s); + return {}; + } + + auto* structure = globalObject->typedArrayStructure(JSC::TypeUint8, impl->isResizableOrGrowableShared()); + size_t byteLength = impl->byteLength(); + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, WTF::move(impl), 0, byteLength)); + } + auto* view = dynamicDowncast(value); if (!view) { - ERR::INVALID_ARG_TYPE_INSTANCE(scope, globalObject, argName, "string"_s, "Buffer, TypedArray, or DataView"_s, value); + ERR::INVALID_ARG_TYPE_INSTANCE(scope, globalObject, argName, "string"_s, "ArrayBuffer, Buffer, TypedArray, or DataView"_s, value); return {}; } @@ -953,7 +974,7 @@ void parseKeyEncoding(JSGlobalObject* globalObject, ThrowScope& scope, JSObject* RETURN_IF_EXCEPTION(scope, ); GCOwnedDataScope cipherView = cipherString->view(globalObject); RETURN_IF_EXCEPTION(scope, ); - config.cipher = getCipherByName(cipherView); + config.cipher = Cipher::FromName(cipherView); if (config.cipher == nullptr) { ERR::CRYPTO_UNKNOWN_CIPHER(scope, globalObject, cipherView); return; diff --git a/src/jsc/bindings/node/crypto/CryptoUtil.h b/src/jsc/bindings/node/crypto/CryptoUtil.h index 4377baa6975b..4d3d09c3749e 100644 --- a/src/jsc/bindings/node/crypto/CryptoUtil.h +++ b/src/jsc/bindings/node/crypto/CryptoUtil.h @@ -58,7 +58,8 @@ int32_t getPadding(JSC::JSGlobalObject* globalObject, JSC::ThrowScope&, JSValue std::optional getSaltLength(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSValue options); DSASigEnc getDSASigEnc(JSC::JSGlobalObject* globalObject, JSC::ThrowScope&, JSValue options); bool convertP1363ToDER(const ncrypto::Buffer& p1363Sig, const ncrypto::EVPKeyPointer& pkey, WTF::Vector& derBuffer); -GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* globalObject, ThrowScope& scope, JSValue dataValue, ASCIILiteral argName, JSValue encodingValue, bool arrayBufferViewOnly = false); +std::optional getInputEncoding(JSGlobalObject* globalObject, ThrowScope& scope, JSValue encodingValue); +GCOwnedDataScope> getArrayBufferOrView2(JSGlobalObject* globalObject, ThrowScope& scope, JSValue dataValue, ASCIILiteral argName, JSValue encodingValue); JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, ASCIILiteral argName, JSValue encodingValue, bool defaultBufferEncoding = false); JSC::JSArrayBufferView* getArrayBufferOrView(JSGlobalObject* globalObject, ThrowScope& scope, JSValue value, ASCIILiteral argName, BufferEncodingType encoding); bool isKeyValidForCurve(const EC_GROUP* group, const ncrypto::BignumPointer& privateKey); diff --git a/src/jsc/bindings/node/crypto/DiffieHellmanFunctions.h b/src/jsc/bindings/node/crypto/DiffieHellmanFunctions.h index 7931b6d7aa97..43d05e5d36c1 100644 --- a/src/jsc/bindings/node/crypto/DiffieHellmanFunctions.h +++ b/src/jsc/bindings/node/crypto/DiffieHellmanFunctions.h @@ -81,18 +81,21 @@ JSC::EncodedJSValue jsDiffieHellmanProtoFuncComputeSecretTemplate(JSC::JSGlobalO auto& dh = thisObject->getImpl(); // Check the public key against our DH parameters - auto checkResult = dh.checkPublicKey(publicKey); - if (checkResult != ncrypto::DHPointer::CheckPublicKeyResult::NONE) { - switch (checkResult) { - // TOO_SMALL/TOO_LARGE only exist on OpenSSL builds; BoringSSL reports them as INVALID. - case ncrypto::DHPointer::CheckPublicKeyResult::INVALID: - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, "Supplied key is invalid"_s); - return {}; - case ncrypto::DHPointer::CheckPublicKeyResult::CHECK_FAILED: - default: - throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, "Unspecified validation error"_s); - return {}; - } + switch (dh.checkPublicKey(publicKey)) { + case ncrypto::DHPointer::CheckPublicKeyResult::CHECK_FAILED: + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, "Unspecified validation error"_s); + return {}; + case ncrypto::DHPointer::CheckPublicKeyResult::TOO_SMALL: + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYLEN, "Supplied key is too small"_s); + return {}; + case ncrypto::DHPointer::CheckPublicKeyResult::TOO_LARGE: + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYLEN, "Supplied key is too large"_s); + return {}; + case ncrypto::DHPointer::CheckPublicKeyResult::INVALID: + throwError(globalObject, scope, ErrorCode::ERR_CRYPTO_INVALID_KEYTYPE, "Supplied key is invalid"_s); + return {}; + case ncrypto::DHPointer::CheckPublicKeyResult::NONE: + break; } // Compute the shared secret diff --git a/src/jsc/bindings/node/crypto/JSCipherPrototype.cpp b/src/jsc/bindings/node/crypto/JSCipherPrototype.cpp index aa9b38e3b67c..25d5319668b5 100644 --- a/src/jsc/bindings/node/crypto/JSCipherPrototype.cpp +++ b/src/jsc/bindings/node/crypto/JSCipherPrototype.cpp @@ -53,7 +53,21 @@ JSC_DEFINE_HOST_FUNCTION(jsCipherUpdate, (JSC::JSGlobalObject * lexicalGlobalObj JSValue dataValue = callFrame->argument(0); JSValue encodingValue = callFrame->argument(1); - JSArrayBufferView* dataView = getArrayBufferOrView(lexicalGlobalObject, scope, dataValue, "data"_s, encodingValue); + // https://github.com/nodejs/node/blob/v26.12.1/lib/internal/crypto/cipher.js#L185-L191 + // An encoding that is not known is UTF-8, as in the other update() methods. + auto encoding = BufferEncodingType::utf8; + if (dataValue.isString()) { + encoding = parseEnumeration(*lexicalGlobalObject, encodingValue).value_or(BufferEncodingType::utf8); + RETURN_IF_EXCEPTION(scope, {}); + unsigned length = asString(dataValue)->length(); + if (encoding == BufferEncodingType::hex && length % 2 != 0) { + return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "encoding"_s, encodingValue, makeString("is invalid for data of length "_s, length)); + } + } else if (!dataValue.inherits()) { + return ERR::INVALID_ARG_TYPE_INSTANCE(scope, lexicalGlobalObject, "data"_s, "string"_s, "Buffer, TypedArray, or DataView"_s, dataValue); + } + + JSArrayBufferView* dataView = getArrayBufferOrView(lexicalGlobalObject, scope, dataValue, "data"_s, encoding); RETURN_IF_EXCEPTION(scope, {}); // Read once: the length of a length-tracking view is not fixed. diff --git a/src/jsc/bindings/node/crypto/JSECDHConstructor.cpp b/src/jsc/bindings/node/crypto/JSECDHConstructor.cpp index d8b125555a14..345e149fbe5b 100644 --- a/src/jsc/bindings/node/crypto/JSECDHConstructor.cpp +++ b/src/jsc/bindings/node/crypto/JSECDHConstructor.cpp @@ -106,6 +106,8 @@ JSC_DEFINE_HOST_FUNCTION(jsECDHConvertKey, (JSC::JSGlobalObject * lexicalGlobalO RETURN_IF_EXCEPTION(scope, {}); auto buffer = keyView->span(); + if (buffer.empty()) + return JSValue::encode(jsEmptyString(vm)); int nid = curveNidFromShortName(curveName); if (nid == NID_undef) diff --git a/src/jsc/bindings/node/crypto/JSHash.cpp b/src/jsc/bindings/node/crypto/JSHash.cpp index fcc7c86a1bc8..e8f5c67f438a 100644 --- a/src/jsc/bindings/node/crypto/JSHash.cpp +++ b/src/jsc/bindings/node/crypto/JSHash.cpp @@ -1,12 +1,14 @@ #include "JSHash.h" #include "CryptoUtil.h" #include "BunClientData.h" +#include "Bindgen/JSBytesView.h" #include #include #include #include #include #include +#include #include #include #include @@ -320,6 +322,110 @@ JSC_DEFINE_HOST_FUNCTION(jsHashProtoFuncDigest, (JSC::JSGlobalObject * lexicalGl RELEASE_AND_RETURN(scope, StringBytes::encode(lexicalGlobalObject, scope, std::span { reinterpret_cast(hash->m_digest.data()), len }, encoding)); } +// oneShotDigest(algorithm, input, outputEncoding, outputLength, hasDigestOptions), which crypto.hash() has validated. +// https://github.com/nodejs/node/blob/v26.12.1/src/crypto/crypto_hash.cc#L362-L501 +JSC_DEFINE_HOST_FUNCTION(jsOneShotDigest, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + JSC::VM& vm = lexicalGlobalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + ncrypto::ClearErrorOnReturn clearErrorOnReturn; + + // Not in Node: a String object, an ArrayBuffer or a Blob, which Bun.CryptoHasher.hash() takes too. + JSValue inputValue = callFrame->argument(1); + if (auto* stringObject = dynamicDowncast(inputValue)) [[unlikely]] { + inputValue = stringObject->internalValue(); + } + if (!inputValue.isString() && !inputValue.inherits() && !inputValue.inherits()) [[unlikely]] { + bool isBlob = Bindgen::checkInMemoryBlob(*lexicalGlobalObject, inputValue); + RETURN_IF_EXCEPTION(scope, {}); + if (!isBlob) { + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "input"_s, "string or an instance of Buffer, TypedArray, or DataView"_s, inputValue); + } + } + + WTF::String algorithm = callFrame->argument(0).toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + // The message holds `algorithm`, which can be as long as a string gets. + auto throwError = [&](WTF::String&& message) -> EncodedJSValue { + if (message.isNull()) { + throwOutOfMemoryError(lexicalGlobalObject, scope); + } else { + throwException(lexicalGlobalObject, scope, createError(lexicalGlobalObject, message)); + } + return {}; + }; + + const EVP_MD* md = ncrypto::getDigestByName(algorithm); + std::unique_ptr zigHasher(md ? nullptr : ExternZigHash::getByName(algorithm), ExternZigHash::destroy); + if (!md && !zigHasher) [[unlikely]] { + return throwError(tryMakeString("Digest method "_s, algorithm, " is not supported"_s)); + } + + auto encoding = parseEnumeration(*lexicalGlobalObject, callFrame->argument(2)).value_or(BufferEncodingType::buffer); + RETURN_IF_EXCEPTION(scope, {}); + + uint32_t digestSize = md ? EVP_MD_size(md) : ExternZigHash::getDigestSize(zigHasher.get()); + uint32_t outputLength = digestSize; + JSValue outputLengthValue = callFrame->argument(3); + if (!outputLengthValue.isUndefined()) { + outputLength = outputLengthValue.toUInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + bool isXof = !md && ExternZigHash::isXof(zigHasher.get()); + if (!isXof && outputLength != digestSize) { + return throwError(tryMakeString("Output length "_s, outputLength, " is invalid for "_s, algorithm, ", which does not support XOF"_s)); + } + } + + // They are for cSHAKE, which BoringSSL does not have. + if (callFrame->argument(4).isTrue()) { + throwCryptoError(lexicalGlobalObject, scope, ERR_get_error(), "Digest options are not supported"_s); + return {}; + } + + if (outputLength == 0) { + RELEASE_AND_RETURN(scope, StringBytes::encode(lexicalGlobalObject, scope, {}, encoding)); + } + + auto digest = [&](std::span input) -> EncodedJSValue { + if (md) { + std::array output; + if (!EVP_Digest(input.data(), input.size(), output.data(), nullptr, md, nullptr)) { + throwCryptoError(lexicalGlobalObject, scope, ERR_get_error()); + return {}; + } + RELEASE_AND_RETURN(scope, StringBytes::encode(lexicalGlobalObject, scope, std::span { output }.first(outputLength), encoding)); + } + + auto output = ncrypto::DataPointer::Alloc(std::max(EVP_MAX_MD_SIZE, outputLength)); + if (!output) { + throwOutOfMemoryError(lexicalGlobalObject, scope); + return {}; + } + std::span bytes { output.get(), output.size() }; + if ((!input.empty() && !ExternZigHash::update(zigHasher.get(), input)) || ExternZigHash::digest(zigHasher.get(), bytes) < outputLength) { + throwCryptoError(lexicalGlobalObject, scope, ERR_get_error()); + return {}; + } + RELEASE_AND_RETURN(scope, StringBytes::encode(lexicalGlobalObject, scope, bytes.first(outputLength), encoding)); + }; + + if (!inputValue.isString()) { + return digest(Bindgen::bytesOfBlobOrBuffer(inputValue)); + } + + auto inputView = asString(inputValue)->view(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (inputView->is8Bit() && WTF::charactersAreAllASCII(inputView->span8())) { + return digest(byteCast(inputView->span8())); + } + + JSValue converted = JSValue::decode(WebCore::constructFromEncoding(lexicalGlobalObject, inputView, BufferEncodingType::utf8)); + RETURN_IF_EXCEPTION(scope, {}); + JSC::EnsureStillAliveScope keepAlive(converted); + return digest(uncheckedDowncast(converted)->span()); +} + JSC_DEFINE_HOST_FUNCTION(constructHash, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { JSC::VM& vm = globalObject->vm(); diff --git a/src/jsc/bindings/node/crypto/JSHash.h b/src/jsc/bindings/node/crypto/JSHash.h index 7787dd382558..8de47898f7f2 100644 --- a/src/jsc/bindings/node/crypto/JSHash.h +++ b/src/jsc/bindings/node/crypto/JSHash.h @@ -129,6 +129,7 @@ class JSHashConstructor final : public JSC::InternalFunction { JSC_DECLARE_HOST_FUNCTION(jsHashProtoFuncUpdate); JSC_DECLARE_HOST_FUNCTION(jsHashProtoFuncDigest); +JSC_DECLARE_HOST_FUNCTION(jsOneShotDigest); void setupJSHashClassStructure(JSC::LazyClassStructure::Initializer& init); diff --git a/src/jsc/bindings/node/crypto/JSSign.cpp b/src/jsc/bindings/node/crypto/JSSign.cpp index a7c6506096f4..0b9e7089b854 100644 --- a/src/jsc/bindings/node/crypto/JSSign.cpp +++ b/src/jsc/bindings/node/crypto/JSSign.cpp @@ -226,6 +226,11 @@ void updateWithBufferView(JSGlobalObject* globalObject, JSSign* sign, JSC::JSArr .len = byteLength, }; + if (!sign->m_mdCtx) { + ERR::CRYPTO_INVALID_STATE(scope, globalObject, "Not initialised"_s); + return; + } + ncrypto::ClearErrorOnReturn clearErrorOnReturn; if (!sign->m_mdCtx.digestUpdate(buffer)) { throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); @@ -301,7 +306,7 @@ JSUint8Array* signWithKey(JSC::JSGlobalObject* lexicalGlobalObject, JSSign* this // Check if the context is initialized if (!thisObject->m_mdCtx) { - throwTypeError(lexicalGlobalObject, scope, "Sign.prototype.sign cannot be called before Sign.prototype.init"_s); + ERR::CRYPTO_INVALID_STATE(scope, lexicalGlobalObject, "Not initialised"_s); return nullptr; } @@ -366,7 +371,7 @@ JSUint8Array* signWithKey(JSC::JSGlobalObject* lexicalGlobalObject, JSSign* this }; if (!pkctx.signInto(data, &sigBuf)) { - throwTypeError(lexicalGlobalObject, scope, "Failed to create signature"_s); + throwCryptoError(lexicalGlobalObject, scope, ERR_get_error(), "Failed to create signature"_s); return nullptr; } @@ -430,10 +435,6 @@ JSC_DEFINE_HOST_FUNCTION(jsSignProtoFuncSign, (JSC::JSGlobalObject * lexicalGlob return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "key"_s, "ArrayBuffer, Buffer, TypedArray, DataView, string, KeyObject, or CryptoKey"_s, options); } - JSValue outputEncodingValue = callFrame->argument(1); - auto outputEncoding = parseEnumeration(*lexicalGlobalObject, outputEncodingValue).value_or(BufferEncodingType::buffer); - RETURN_IF_EXCEPTION(scope, {}); - // Get RSA padding mode and salt length if applicable int32_t padding = getPadding(lexicalGlobalObject, scope, options, {}); RETURN_IF_EXCEPTION(scope, {}); @@ -466,11 +467,18 @@ JSC_DEFINE_HOST_FUNCTION(jsSignProtoFuncSign, (JSC::JSGlobalObject * lexicalGlob const ncrypto::EVPKeyPointer& keyPtr = keyObject.asymmetricKey(); + if (keyPtr.isOneShotVariant()) { + return ERR::CRYPTO_UNSUPPORTED_OPERATION(scope, lexicalGlobalObject); + } + // Use the signWithKey function to perform the signing operation JSUint8Array* signature = signWithKey(lexicalGlobalObject, thisObject, keyPtr, dsaSigEnc, padding, saltLen); EXCEPTION_ASSERT(!!signature == !scope.exception()); RETURN_IF_EXCEPTION(scope, {}); + auto outputEncoding = getEncodingDefaultBuffer(lexicalGlobalObject, scope, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + // If output encoding is not buffer, convert the signature to the requested encoding if (outputEncoding != BufferEncodingType::buffer) { EncodedJSValue encodedSignature = jsBufferToString(lexicalGlobalObject, scope, signature, 0, signature->byteLength(), outputEncoding); diff --git a/src/jsc/bindings/node/crypto/JSVerify.cpp b/src/jsc/bindings/node/crypto/JSVerify.cpp index 255fb6993ea1..92d0216dc648 100644 --- a/src/jsc/bindings/node/crypto/JSVerify.cpp +++ b/src/jsc/bindings/node/crypto/JSVerify.cpp @@ -270,6 +270,10 @@ JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncUpdate, (JSGlobalObject * globalObject .len = byteLength, }; + if (!thisObject->m_mdCtx) { + return ERR::CRYPTO_INVALID_STATE(scope, globalObject, "Not initialised"_s); + } + ncrypto::ClearErrorOnReturn clearErrorOnReturn; if (!thisObject->m_mdCtx.digestUpdate(buffer)) { throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); @@ -295,6 +299,10 @@ JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncUpdate, (JSGlobalObject * globalObject .len = byteLength, }; + if (!thisObject->m_mdCtx) { + return ERR::CRYPTO_INVALID_STATE(scope, globalObject, "Not initialised"_s); + } + ncrypto::ClearErrorOnReturn clearErrorOnReturn; if (!thisObject->m_mdCtx.digestUpdate(buffer)) { throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to update digest"); @@ -318,12 +326,6 @@ JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncVerify, (JSGlobalObject * globalObject return {}; } - // Check if the context is initialized - if (!thisObject->m_mdCtx) { - throwTypeError(globalObject, scope, "Verify.prototype.verify cannot be called before Verify.prototype.init"_s); - return {}; - } - // This function receives two arguments: options and signature JSValue options = callFrame->argument(0); JSValue signatureValue = callFrame->argument(1); @@ -364,6 +366,28 @@ JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncVerify, (JSGlobalObject * globalObject DSASigEnc dsaSigEnc = getDSASigEnc(globalObject, scope, options); RETURN_IF_EXCEPTION(scope, {}); + // https://github.com/nodejs/node/blob/v26.12.1/src/crypto/crypto_sig.cc#L530-L607 + if (keyPtr.isOneShotVariant()) { + return ERR::CRYPTO_UNSUPPORTED_OPERATION(scope, globalObject); + } + + ncrypto::Buffer sigBuf { + .data = static_cast(signatureBuffer->vector()), + .len = signatureBuffer->byteLength(), + }; + + WTF::Vector derBuffer; + if (dsaSigEnc == DSASigEnc::P1363 && keyPtr.isSigVariant()) { + if (!convertP1363ToDER(sigBuf, keyPtr, derBuffer)) { + return ERR::CRYPTO_OPERATION_FAILED(scope, globalObject, "Malformed signature"_s); + } + sigBuf = { .data = derBuffer.begin(), .len = derBuffer.size() }; + } + + if (!thisObject->m_mdCtx) { + return ERR::CRYPTO_INVALID_STATE(scope, globalObject, "Not initialised"_s); + } + // Move mdCtx out of JSVerify object to finalize it ncrypto::EVPMDCtxPointer mdCtx = WTF::move(thisObject->m_mdCtx); thisObject->m_sizeForGC = 0; @@ -381,63 +405,24 @@ JSC_DEFINE_HOST_FUNCTION(jsVerifyProtoFuncVerify, (JSGlobalObject * globalObject return {}; } - // Create verification context + // Create verification context. A key that cannot verify is -2 from OpenSSL, which Node throws for, and 0 from BoringSSL. auto pkctx = keyPtr.newCtx(); if (!pkctx || pkctx.initForVerify() <= 0) { throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to initialize verification context"_s); return {}; } - // Set RSA padding mode and salt length if applicable - if (keyPtr.isRsaVariant()) { - std::optional effective_salt_len = saltLen; - - // For PSS padding without explicit salt length for verification, - // we don't need to calculate - use RSA_PSS_SALTLEN_AUTO (-2) to auto-detect - // This matches Node.js behavior for verification - if (padding == RSA_PKCS1_PSS_PADDING && !saltLen.has_value()) { - effective_salt_len = RSA_PSS_SALTLEN_AUTO; - } - - if (!ncrypto::EVPKeyCtxPointer::setRsaPadding(pkctx.get(), padding, effective_salt_len)) { - throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set RSA padding"_s); - return {}; - } - } - - // Set signature MD from the digest context - if (!pkctx.setSignatureMd(mdCtx)) { - throwCryptoError(globalObject, scope, ERR_get_error(), "Failed to set signature message digest"_s); - return {}; - } - - // Handle P1363 format conversion for EC keys if needed - ncrypto::Buffer sigBuf { - .data = static_cast(signatureBuffer->vector()), - .len = signatureBuffer->byteLength(), - }; - - if (dsaSigEnc == DSASigEnc::P1363 && keyPtr.isSigVariant()) { - WTF::Vector derBuffer; - - // If the signature cannot be converted to DER (e.g. its length is not - // 2 * bytesOfRS), fail verification instead of reinterpreting the raw - // bytes as a DER signature, matching Node.js. - if (!convertP1363ToDER(sigBuf, keyPtr, derBuffer)) { - return JSValue::encode(jsBoolean(false)); - } - - ncrypto::Buffer derSigBuf { - .data = derBuffer.begin(), - .len = derBuffer.size(), - }; - - bool result = pkctx.verify(derSigBuf, data); - return JSValue::encode(jsBoolean(result)); + // For PSS padding without explicit salt length for verification, + // we don't need to calculate - use RSA_PSS_SALTLEN_AUTO (-2) to auto-detect + // This matches Node.js behavior for verification + if (padding == RSA_PKCS1_PSS_PADDING && !saltLen.has_value()) { + saltLen = RSA_PSS_SALTLEN_AUTO; } - // Perform verification with the original signature - bool result = pkctx.verify(sigBuf, data); + // Options that the key does not take are a signature that does not verify, not an error. + bool result = (!keyPtr.isRsaVariant() || ncrypto::EVPKeyCtxPointer::setRsaPadding(pkctx.get(), padding, saltLen)) + && pkctx.setSignatureMd(mdCtx) + && pkctx.verify(sigBuf, data); return JSValue::encode(jsBoolean(result)); } diff --git a/src/jsc/bindings/node/crypto/KeyObject.cpp b/src/jsc/bindings/node/crypto/KeyObject.cpp index b878979d0116..0513aca0b269 100644 --- a/src/jsc/bindings/node/crypto/KeyObject.cpp +++ b/src/jsc/bindings/node/crypto/KeyObject.cpp @@ -176,6 +176,13 @@ JSC::JSValue KeyObject::exportJwkEdKey(JSC::JSGlobalObject* lexicalGlobalObject, } })(); + auto ktyKey = commonStrings.jwkKtyString()->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + jwk->putDirect( + vm, + Identifier::fromString(vm, ktyKey), + commonStrings.jwkOkpString()); + auto crvKey = commonStrings.jwkCrvString()->value(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, {}); jwk->putDirect( @@ -183,6 +190,16 @@ JSC::JSValue KeyObject::exportJwkEdKey(JSC::JSGlobalObject* lexicalGlobalObject, Identifier::fromString(vm, crvKey), jsString(vm, makeString(curve))); + ncrypto::DataPointer publicData = pkey.rawPublicKey(); + JSValue encoded = JSValue::decode(StringBytes::encode(lexicalGlobalObject, scope, publicData.span(), BufferEncodingType::base64url)); + RETURN_IF_EXCEPTION(scope, {}); + auto xKey = commonStrings.jwkXString()->value(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + jwk->putDirect( + vm, + Identifier::fromString(vm, xKey), + encoded); + if (exportType == CryptoKeyType::Private) { ncrypto::DataPointer privateData = pkey.rawPrivateKey(); @@ -196,21 +213,6 @@ JSC::JSValue KeyObject::exportJwkEdKey(JSC::JSGlobalObject* lexicalGlobalObject, encoded); } - ncrypto::DataPointer publicData = pkey.rawPublicKey(); - JSValue encoded = JSValue::decode(StringBytes::encode(lexicalGlobalObject, scope, publicData.span(), BufferEncodingType::base64url)); - RETURN_IF_EXCEPTION(scope, {}); - auto xKey = commonStrings.jwkXString()->value(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); - jwk->putDirect( - vm, - Identifier::fromString(vm, xKey), - encoded); - - jwk->putDirect( - vm, - Identifier::fromString(vm, commonStrings.jwkKtyString()->value(lexicalGlobalObject)), - commonStrings.jwkOkpString()); - return jwk; } @@ -644,6 +646,10 @@ JSValue KeyObject::exportAsymmetric(JSGlobalObject* globalObject, ThrowScope& sc ncrypto::EVPKeyPointer::PKFormatType rawFormat; if (formatView == "raw-public"_s) { + if (exportType != CryptoKeyType::Public) { + ERR::INVALID_ARG_VALUE(scope, globalObject, "options.format"_s, formatValue); + return {}; + } rawFormat = ncrypto::EVPKeyPointer::PKFormatType::RawPublic; } else if (formatView == "raw-private"_s) { if (exportType != CryptoKeyType::Private) { @@ -1290,10 +1296,6 @@ __attribute__((minsize)) KeyObject KeyObject::getKeyObjectHandleFromJwk(JSGlobal // The JWK itself decides whether private key material is present. CryptoKeyType jwkType = privValue.isString() ? CryptoKeyType::Private : CryptoKeyType::Public; - if (keyType == CryptoKeyType::Private && jwkType == CryptoKeyType::Public) { - ERR::CRYPTO_INVALID_JWK(scope, globalObject, "JWK does not contain private key material"_s); - return {}; - } // pubValue / privValue were already read and type-checked above; decode // them directly so each JWK property is observed exactly once. @@ -1324,65 +1326,77 @@ __attribute__((minsize)) KeyObject KeyObject::getKeyObjectHandleFromJwk(JSGlobal return {}; } + if (keyType == CryptoKeyType::Private && jwkType == CryptoKeyType::Public) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, "JWK does not contain private key material"_s); + return {}; + } + JSC::ensureStillAliveHere(pubBuf); JSC::ensureStillAliveHere(privBuf); return create(keyType, WTF::move(key)); } case Kty::Ec: { - auto crvView = getJwkStringView(globalObject, scope, jwk, "crv"_s, "key.crv"_s); + // Node's ImportJWKEcKey: https://github.com/nodejs/node/blob/v26.12.1/src/crypto/crypto_ec.cc#L550 + const ASCIILiteral invalidKey = "Invalid JWK EC key"_s; + VM& vm = globalObject->vm(); + JSValue crvValue = jwk->get(globalObject, Identifier::fromString(vm, "crv"_s)); RETURN_IF_EXCEPTION(scope, {}); + if (!crvValue.isString()) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, invalidKey); + return {}; + } - if (crvView != "P-256"_s && crvView != "secp256k1"_s && crvView != "P-384"_s && crvView != "P-521"_s) { - // validateOneOf - ERR::INVALID_ARG_VALUE(scope, globalObject, "key.crv"_s, crvView.owner, "must be one of: 'P-256', 'secp256k1', 'P-384', 'P-521'"_s); + auto crvView = asString(crvValue)->view(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + int nid = Ec::GetCurveIdFromName(crvView); + if (nid == NID_undef) { + ERR::CRYPTO_INVALID_CURVE(scope, globalObject); return {}; } - auto xView = getJwkStringView(globalObject, scope, jwk, "x"_s, "key.x"_s); + JSValue xValue = jwk->get(globalObject, Identifier::fromString(vm, "x"_s)); RETURN_IF_EXCEPTION(scope, {}); - auto yView = getJwkStringView(globalObject, scope, jwk, "y"_s, "key.y"_s); + JSValue yValue = jwk->get(globalObject, Identifier::fromString(vm, "y"_s)); + RETURN_IF_EXCEPTION(scope, {}); + JSValue dValue = jwk->get(globalObject, Identifier::fromString(vm, "d"_s)); RETURN_IF_EXCEPTION(scope, {}); - GCOwnedDataScope dView = GCOwnedDataScope(nullptr, WTF::nullStringView()); - - if (keyType != CryptoKeyType::Public) { - dView = getJwkStringView(globalObject, scope, jwk, "d"_s, "key.d"_s); - RETURN_IF_EXCEPTION(scope, {}); + if (!xValue.isString() || !yValue.isString() || (!dValue.isUndefined() && !dValue.isString())) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, invalidKey); + return {}; } - MarkPopErrorOnReturn markPopError; + // The JWK itself decides whether private key material is present. + const bool isPrivate = dValue.isString(); - int nid = Ec::GetCurveIdFromName(crvView); - if (nid == NID_undef) { - ERR::CRYPTO_INVALID_CURVE(scope, globalObject); - return {}; - } + MarkPopErrorOnReturn markPopError; auto ec = ECKeyPointer::NewByCurveName(nid); if (!ec) { - ERR::CRYPTO_INVALID_JWK(scope, globalObject); + ERR::CRYPTO_INVALID_JWK(scope, globalObject, invalidKey); return {}; } - auto* xBuf = decodeJwkString(globalObject, scope, xView, "key.x"_s); + BignumPointer x = jwkStringToBn(globalObject, scope, xValue, "key.x"_s); RETURN_IF_EXCEPTION(scope, {}); - auto* yBuf = decodeJwkString(globalObject, scope, yView, "key.y"_s); + BignumPointer y = jwkStringToBn(globalObject, scope, yValue, "key.y"_s); RETURN_IF_EXCEPTION(scope, {}); - if (!ec.setPublicKeyRaw(jwkBufToBn(xBuf), jwkBufToBn(yBuf))) { - ERR::CRYPTO_INVALID_JWK(scope, globalObject, "Invalid JWK EC key"_s); + if (!ec.setPublicKeyRaw(x, y)) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, invalidKey); return {}; } - if (keyType != CryptoKeyType::Public) { - auto* dBuf = decodeJwkString(globalObject, scope, dView, "key.d"_s); + if (isPrivate) { + BignumPointer d = jwkStringToBn(globalObject, scope, dValue, "key.d"_s); RETURN_IF_EXCEPTION(scope, {}); - auto dBufSpan = dBuf->span(); - BignumPointer dBn = BignumPointer(dBufSpan.data(), dBufSpan.size()); - if (!ec.setPrivateKey(dBn) || !ec.checkKey()) { - ERR::CRYPTO_INVALID_JWK(scope, globalObject, "Invalid JWK EC key"_s); + if (!ec.setPrivateKey(d) || !ec.checkKey()) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, invalidKey); return {}; } + } else if (keyType == CryptoKeyType::Private) { + ERR::CRYPTO_INVALID_JWK(scope, globalObject, "JWK does not contain private key material"_s); + return {}; } auto key = EVPKeyPointer::New(); @@ -1784,7 +1798,7 @@ KeyObject KeyObject::getPublicOrPrivateKey( return {}; } -KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue, PrepareAsymmetricKeyMode mode) +KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue, PrepareAsymmetricKeyMode mode, WTF::ASCIILiteral name) { VM& vm = globalObject->vm(); @@ -1925,8 +1939,12 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo RETURN_IF_EXCEPTION(scope, {}); if (formatView == "jwk"_s) { - V::validateObject(scope, globalObject, dataValue, "key.key"_s); + bool isArray = JSC::isArray(globalObject, dataValue); RETURN_IF_EXCEPTION(scope, {}); + if (!dataValue.isObject() || isArray || dataValue.isCallable()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, makeString(name, ".key"_s), "object"_s, dataValue); + return {}; + } JSObject* jwk = dataValue.getObject(); KeyObject handle = getKeyObjectHandleFromJwk(globalObject, scope, jwk, mode); RETURN_IF_EXCEPTION(scope, {}); @@ -1935,19 +1953,21 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo if (formatView == "raw-public"_s || formatView == "raw-private"_s || formatView == "raw-seed"_s) { if ((mode == PrepareAsymmetricKeyMode::ConsumePrivate || mode == PrepareAsymmetricKeyMode::CreatePrivate) && formatView == "raw-public"_s) { - ERR::INVALID_ARG_VALUE(scope, globalObject, "key.format"_s, formatValue); + ERR::INVALID_ARG_VALUE(scope, globalObject, makeString(name, ".format"_s), formatValue); return {}; } if (!dynamicDowncast(dataValue) && !dynamicDowncast(dataValue)) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "key.key"_s, "ArrayBuffer, Buffer, TypedArray, or DataView"_s, dataValue); + ERR::INVALID_ARG_INSTANCE(scope, globalObject, makeString(name, ".key"_s), "ArrayBuffer, Buffer, TypedArray, or DataView"_s, dataValue); return {}; } JSValue typeValue = keyObj->get(globalObject, Identifier::fromString(vm, "asymmetricKeyType"_s)); RETURN_IF_EXCEPTION(scope, {}); - V::validateString(scope, globalObject, typeValue, "key.asymmetricKeyType"_s); - RETURN_IF_EXCEPTION(scope, {}); + if (!typeValue.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, makeString(name, ".asymmetricKeyType"_s), "string"_s, typeValue); + return {}; + } auto typeStr = typeValue.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, {}); @@ -1955,8 +1975,10 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo if (typeStr == "ec"_s) { namedCurveValue = keyObj->get(globalObject, Identifier::fromString(vm, "namedCurve"_s)); RETURN_IF_EXCEPTION(scope, {}); - V::validateString(scope, globalObject, namedCurveValue, "key.namedCurve"_s); - RETURN_IF_EXCEPTION(scope, {}); + if (!namedCurveValue.isString()) { + ERR::INVALID_ARG_TYPE(scope, globalObject, makeString(name, ".namedCurve"_s), "string"_s, namedCurveValue); + return {}; + } } // Capture the key span only after the last property getter has run, so a user @@ -1990,19 +2012,10 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo auto dataView = dataString->view(globalObject); RETURN_IF_EXCEPTION(scope, {}); - BufferEncodingType encoding = BufferEncodingType::utf8; - if (encodingValue.isString()) { - auto* encodingString = encodingValue.toString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - auto encodingView = encodingString->view(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - - if (encodingView != "buffer"_s) { - encoding = parseEnumerationFromView(encodingView).value_or(BufferEncodingType::utf8); - } - } + auto encoding = getInputEncoding(globalObject, scope, encodingValue); + RETURN_IF_EXCEPTION(scope, {}); - JSValue decoded = JSValue::decode(constructFromEncoding(globalObject, dataView, encoding)); + JSValue decoded = JSValue::decode(constructFromEncoding(globalObject, dataView, *encoding)); RETURN_IF_EXCEPTION(scope, {}); if (auto* decodedView = dynamicDowncast(decoded)) { EVPKeyPointer::PrivateKeyEncodingConfig config; @@ -2049,30 +2062,30 @@ KeyObject::PrepareAsymmetricKeyResult KeyObject::prepareAsymmetricKey(JSC::JSGlo } if (mode != PrepareAsymmetricKeyMode::CreatePrivate) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "key.key"_s, "string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"_s, dataValue); + ERR::INVALID_ARG_TYPE(scope, globalObject, makeString(name, ".key"_s), "string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"_s, dataValue); } else { - ERR::INVALID_ARG_TYPE(scope, globalObject, "key.key"_s, "string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"_s, dataValue); + ERR::INVALID_ARG_TYPE(scope, globalObject, makeString(name, ".key"_s), "string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"_s, dataValue); } return {}; } if (mode != PrepareAsymmetricKeyMode::CreatePrivate) { - ERR::INVALID_ARG_TYPE(scope, globalObject, "key"_s, "string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"_s, keyValue); + ERR::INVALID_ARG_TYPE(scope, globalObject, name, "string or an instance of ArrayBuffer, Buffer, TypedArray, DataView, KeyObject, or CryptoKey"_s, keyValue); } else { - ERR::INVALID_ARG_TYPE(scope, globalObject, "key"_s, "string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"_s, keyValue); + ERR::INVALID_ARG_TYPE(scope, globalObject, name, "string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView"_s, keyValue); } return {}; } -KeyObject::PrepareAsymmetricKeyResult KeyObject::preparePrivateKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyValue) +KeyObject::PrepareAsymmetricKeyResult KeyObject::preparePrivateKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyValue, WTF::ASCIILiteral name) { - return prepareAsymmetricKey(globalObject, scope, keyValue, PrepareAsymmetricKeyMode::ConsumePrivate); + return prepareAsymmetricKey(globalObject, scope, keyValue, PrepareAsymmetricKeyMode::ConsumePrivate, name); } -KeyObject::PrepareAsymmetricKeyResult KeyObject::preparePublicOrPrivateKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyValue) +KeyObject::PrepareAsymmetricKeyResult KeyObject::preparePublicOrPrivateKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyValue, WTF::ASCIILiteral name) { - return prepareAsymmetricKey(globalObject, scope, keyValue, PrepareAsymmetricKeyMode::ConsumePublic); + return prepareAsymmetricKey(globalObject, scope, keyValue, PrepareAsymmetricKeyMode::ConsumePublic, name); } KeyObject KeyObject::prepareSecretKey(JSGlobalObject* globalObject, ThrowScope& scope, JSValue keyValue, JSValue encodingValue, bool bufferOnly) @@ -2108,10 +2121,10 @@ KeyObject KeyObject::prepareSecretKey(JSGlobalObject* globalObject, ThrowScope& auto keyView = keyString->view(globalObject); RETURN_IF_EXCEPTION(scope, {}); - BufferEncodingType encoding = parseEnumerationAllowBuffer(*globalObject, encodingValue).value_or(BufferEncodingType::utf8); + auto encoding = getInputEncoding(globalObject, scope, encodingValue); RETURN_IF_EXCEPTION(scope, {}); - JSValue buffer = JSValue::decode(constructFromEncoding(globalObject, keyView, encoding)); + JSValue buffer = JSValue::decode(constructFromEncoding(globalObject, keyView, *encoding)); RETURN_IF_EXCEPTION(scope, {}); if (buffer.isEmpty()) { diff --git a/src/jsc/bindings/node/crypto/KeyObject.h b/src/jsc/bindings/node/crypto/KeyObject.h index 89d2e36229d0..72d74e50642b 100644 --- a/src/jsc/bindings/node/crypto/KeyObject.h +++ b/src/jsc/bindings/node/crypto/KeyObject.h @@ -83,9 +83,9 @@ class KeyObject { std::optional passphrase = { std::nullopt }; }; - static PrepareAsymmetricKeyResult prepareAsymmetricKey(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue keyValue, PrepareAsymmetricKeyMode mode); - static PrepareAsymmetricKeyResult preparePrivateKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue); - static PrepareAsymmetricKeyResult preparePublicOrPrivateKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue); + static PrepareAsymmetricKeyResult prepareAsymmetricKey(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue keyValue, PrepareAsymmetricKeyMode mode, WTF::ASCIILiteral name = "key"_s); + static PrepareAsymmetricKeyResult preparePrivateKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue, WTF::ASCIILiteral name = "key"_s); + static PrepareAsymmetricKeyResult preparePublicOrPrivateKey(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSValue keyValue, WTF::ASCIILiteral name = "key"_s); static KeyObject prepareSecretKey(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSValue keyValue, JSC::JSValue encodingValue, bool bufferOnly = false); JSC::JSValue exportJwkEdKey(JSC::JSGlobalObject*, JSC::ThrowScope&, WebCore::CryptoKeyType exportType); diff --git a/src/jsc/bindings/node/crypto/node_crypto_binding.cpp b/src/jsc/bindings/node/crypto/node_crypto_binding.cpp index 08926e6df0cc..57b0e7cb2c3e 100644 --- a/src/jsc/bindings/node/crypto/node_crypto_binding.cpp +++ b/src/jsc/bindings/node/crypto/node_crypto_binding.cpp @@ -48,6 +48,7 @@ JSC_DEFINE_HOST_FUNCTION(jsGetCurves, (JSC::JSGlobalObject * lexicalGlobalObject const size_t numCurves = EC_get_builtin_curves(nullptr, 0); Vector curves(numCurves); EC_get_builtin_curves(curves.begin(), numCurves); + std::ranges::sort(curves, [](const auto& a, const auto& b) { return strcmp(OBJ_nid2sn(a.nid), OBJ_nid2sn(b.nid)) < 0; }); JSArray* result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, numCurves); RETURN_IF_EXCEPTION(scope, {}); @@ -63,6 +64,29 @@ JSC_DEFINE_HOST_FUNCTION(jsGetCurves, (JSC::JSGlobalObject * lexicalGlobalObject return JSValue::encode(result); } +JSC_DEFINE_HOST_FUNCTION(jsGetHashes, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto names = ncrypto::getDigestNames(); + JSArray* result = JSC::constructEmptyArray(lexicalGlobalObject, nullptr, names.size()); + RETURN_IF_EXCEPTION(scope, {}); + + for (size_t i = 0; i < names.size(); i++) { + result->putDirectIndex(lexicalGlobalObject, i, JSC::jsNontrivialString(vm, names[i].name)); + RETURN_IF_EXCEPTION(scope, {}); + } + + return JSValue::encode(result); +} + +// For pbkdf2(), which is in Rust. +extern "C" const EVP_MD* Bun__getDigestByName(const Latin1Character* name, size_t length) +{ + return ncrypto::getDigestByName(WTF::StringView(std::span { name, length })); +} + JSC_DEFINE_HOST_FUNCTION(jsGetCiphers, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) { auto& vm = JSC::getVM(lexicalGlobalObject); @@ -284,24 +308,16 @@ JSC_DEFINE_HOST_FUNCTION(jsGetCipherInfo, (JSC::JSGlobalObject * lexicalGlobalOb } } - JSObject* result = JSC::constructEmptyObject(lexicalGlobalObject); - - if (!modeLabel.isEmpty()) { - Bun::putDirectNamed(vm, result, "mode"_s, jsString(vm, String::fromUTF8(modeLabel))); - } - + // Like Node's, it has no prototype and always has all six properties. + JSObject* result = JSC::constructEmptyObject(vm, lexicalGlobalObject->nullPrototypeObjectStructure()); + Bun::putDirectNamed(vm, result, "mode"_s, modeLabel.isEmpty() ? jsUndefined() : jsString(vm, String::fromUTF8(modeLabel))); Bun::putDirectNamed(vm, result, "name"_s, jsString(vm, name.convertToASCIILowercase())); Bun::putDirectNamed(vm, result, "nid"_s, jsNumber(cipher.getNid())); - - if (!cipher.isStreamMode()) { - Bun::putDirectNamed(vm, result, "blockSize"_s, jsNumber(blockSize)); - } - - if (ivLength != 0) { - Bun::putDirectNamed(vm, result, "ivLength"_s, jsNumber(ivLength)); - } - Bun::putDirectNamed(vm, result, "keyLength"_s, jsNumber(keyLength)); + // Stream ciphers do not have a meaningful block size + Bun::putDirectNamed(vm, result, "blockSize"_s, cipher.isStreamMode() ? jsUndefined() : jsNumber(blockSize)); + // Ciphers that do not use an IV shouldn't report a length + Bun::putDirectNamed(vm, result, "ivLength"_s, ivLength == 0 ? jsUndefined() : jsNumber(ivLength)); return JSValue::encode(result); } @@ -320,6 +336,10 @@ __attribute__((minsize)) JSValue createNodeCryptoBinding(Zig::GlobalObject* glob obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCurves"_s)), JSFunction::create(vm, globalObject, 0, "getCurves"_s, jsGetCurves, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "oneShotDigest"_s)), + JSFunction::create(vm, globalObject, 5, "oneShotDigest"_s, jsOneShotDigest, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getHashes"_s)), + JSFunction::create(vm, globalObject, 0, "getHashes"_s, jsGetHashes, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCiphers"_s)), JSFunction::create(vm, globalObject, 0, "getCiphers"_s, jsGetCiphers, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "getCipherInfo"_s)), diff --git a/src/jsc/bindings/webcore/SerializedScriptValue.cpp b/src/jsc/bindings/webcore/SerializedScriptValue.cpp index 911715fd459b..3a2f6f56fa66 100644 --- a/src/jsc/bindings/webcore/SerializedScriptValue.cpp +++ b/src/jsc/bindings/webcore/SerializedScriptValue.cpp @@ -1352,7 +1352,7 @@ class CloneSerializer : public CloneBase { } #if ENABLE(WEB_CRYPTO) if (auto* key = JSCryptoKey::toWrapped(vm, obj)) { - if (m_forStorage == SerializationForStorage::Yes && !key->extractable()) { + if ((m_forStorage == SerializationForStorage::Yes || m_forTransfer == SerializationForCrossProcessTransfer::Yes) && !key->extractable()) { code = SerializationReturnCode::DataCloneError; return true; } diff --git a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp index 8cd58670276a..23f8cb051c7a 100644 --- a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp +++ b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.cpp @@ -30,13 +30,6 @@ namespace WebCore { -size_t bytesUsedToEncodedLength(uint8_t octet) -{ - if (octet < MaxLengthInOneByte) - return 1; - return octet - MaxLengthInOneByte + 1; -} - static size_t extraBytesNeededForEncodedLength(size_t length) { if (!length) diff --git a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h index b1961fc3fa4c..dc5ce6d92e70 100644 --- a/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h +++ b/src/jsc/bindings/webcrypto/CommonCryptoDERUtilities.h @@ -43,7 +43,6 @@ inline constexpr unsigned char SequenceMark = 0x30; inline constexpr unsigned char InitialOctet = 0x00; inline constexpr size_t MaxLengthInOneByte = 128; -size_t bytesUsedToEncodedLength(uint8_t); void addEncodedASN1Length(Vector&, size_t); } // namespace WebCore diff --git a/src/jsc/bindings/webcrypto/CryptoAlgorithmAKPShared.h b/src/jsc/bindings/webcrypto/CryptoAlgorithmAKPShared.h index b96e8af08708..7398e68e5e26 100644 --- a/src/jsc/bindings/webcrypto/CryptoAlgorithmAKPShared.h +++ b/src/jsc/bindings/webcrypto/CryptoAlgorithmAKPShared.h @@ -113,7 +113,7 @@ inline void importAkpKey(CryptoAlgorithmIdentifier identifier, const AkpAlgorith exceptionCallback(DataError, "Invalid JWK \"use\" Parameter"_s); return; } - if (hasDuplicateJwkKeyOps(jwk.key_ops)) { + if (jwk.hasDuplicateKeyOps) { exceptionCallback(DataError, "Duplicate key operation"_s); return; } diff --git a/src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp b/src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp index db71adc8cd5b..0a523db5dc43 100644 --- a/src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp +++ b/src/jsc/bindings/webcrypto/CryptoAlgorithmChaCha20Poly1305.cpp @@ -158,7 +158,7 @@ void CryptoAlgorithmChaCha20Poly1305::importKey(CryptoKeyFormat format, KeyData& exceptionCallback(DataError, "Invalid JWK \"use\" Parameter"_s); return; } - if (hasDuplicateJwkKeyOps(jwk.key_ops)) { + if (jwk.hasDuplicateKeyOps) { exceptionCallback(DataError, "Duplicate key operation"_s); return; } diff --git a/src/jsc/bindings/webcrypto/CryptoAlgorithmHMAC.cpp b/src/jsc/bindings/webcrypto/CryptoAlgorithmHMAC.cpp index 29ef8533b0f6..e7a2afb1d6fa 100644 --- a/src/jsc/bindings/webcrypto/CryptoAlgorithmHMAC.cpp +++ b/src/jsc/bindings/webcrypto/CryptoAlgorithmHMAC.cpp @@ -175,7 +175,7 @@ void CryptoAlgorithmHMAC::importKey(CryptoKeyFormat format, KeyData&& data, cons exceptionCallback(DataError, "Invalid JWK \"use\" Parameter"_s); return; } - if (hasDuplicateJwkKeyOps(jwk.key_ops)) { + if (jwk.hasDuplicateKeyOps) { exceptionCallback(DataError, "Duplicate key operation"_s); return; } diff --git a/src/jsc/bindings/webcrypto/CryptoAlgorithmRSA_PSSOpenSSL.cpp b/src/jsc/bindings/webcrypto/CryptoAlgorithmRSA_PSSOpenSSL.cpp index 564b2968a3b2..095f33f32eb3 100644 --- a/src/jsc/bindings/webcrypto/CryptoAlgorithmRSA_PSSOpenSSL.cpp +++ b/src/jsc/bindings/webcrypto/CryptoAlgorithmRSA_PSSOpenSSL.cpp @@ -104,8 +104,9 @@ static ExceptionOr verifyWithMD(const CryptoAlgorithmRsaPssParams& paramet if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_PSS_PADDING) <= 0) return Exception { OperationError }; + // No signature has a salt that long. if (setSaltLength(ctx.get(), parameters.saltLength) <= 0) - return Exception { OperationError }; + return false; if (EVP_PKEY_CTX_set_signature_md(ctx.get(), md) <= 0) return Exception { OperationError }; diff --git a/src/jsc/bindings/webcrypto/CryptoKeyECOpenSSL.cpp b/src/jsc/bindings/webcrypto/CryptoKeyECOpenSSL.cpp index 8fb781d90f6f..08c0afa3e659 100644 --- a/src/jsc/bindings/webcrypto/CryptoKeyECOpenSSL.cpp +++ b/src/jsc/bindings/webcrypto/CryptoKeyECOpenSSL.cpp @@ -470,11 +470,13 @@ Vector CryptoKeyEC::platformExportPkcs8() const return {}; // A key that came without its optional public key has a flag that leaves it out again. As in ncrypto's - // Ec::ExportPrivatePkcs8, a copy without the flag is what is encoded. + // Ec::ExportPrivatePkcs8, a copy without the flag is what is encoded. One that came with a compressed point + // would keep that form too. auto ecKey = ECKeyPtr(EC_KEY_dup(EVP_PKEY_get0_EC_KEY(platformKey()))); if (!ecKey) return {}; EC_KEY_set_enc_flags(ecKey.get(), EC_KEY_get_enc_flags(ecKey.get()) & ~EC_PKEY_NO_PUBKEY); + EC_KEY_set_conv_form(ecKey.get(), POINT_CONVERSION_UNCOMPRESSED); auto pkey = EvpPKeyPtr(EVP_PKEY_new()); if (!pkey || EVP_PKEY_set1_EC_KEY(pkey.get(), ecKey.get()) != 1) return {}; diff --git a/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp b/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp index 237e06ebd21d..4d0f4228ef6b 100644 --- a/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp +++ b/src/jsc/bindings/webcrypto/CryptoKeyOKPOpenSSL.cpp @@ -39,22 +39,9 @@ namespace WebCore { -// The OID scans below are hand-rolled byte compares and cannot tell a well-formed -// key of another type from malformed bytes, so ask the real parser before a caller -// reports "Invalid key type" rather than "Invalid keyData". -static bool parsesAsSubjectPublicKeyInfo(const Vector& keyData) +static int evpKeyType(CryptoKeyOKP::NamedCurve namedCurve) { - const uint8_t* ptr = keyData.begin(); - return !!EvpPKeyPtr(d2i_PUBKEY(nullptr, &ptr, keyData.size())); -} - -static bool parsesAsPrivateKeyInfo(const Vector& keyData) -{ - const uint8_t* ptr = keyData.begin(); - auto p8inf = PKCS8PrivKeyInfoPtr(d2i_PKCS8_PRIV_KEY_INFO(nullptr, &ptr, keyData.size())); - if (!p8inf) - return false; - return !!EvpPKeyPtr(EVP_PKCS82PKEY(p8inf.get())); + return namedCurve == CryptoKeyOKP::NamedCurve::Ed25519 ? EVP_PKEY_ED25519 : EVP_PKEY_X25519; } bool CryptoKeyOKP::isPlatformSupportedCurve(NamedCurve namedCurve) @@ -94,70 +81,24 @@ std::optional CryptoKeyOKP::platformGeneratePair(CryptoAlgorithmI // For all of the OIDs, the parameters MUST be absent. RefPtr CryptoKeyOKP::importSpki(CryptoAlgorithmIdentifier identifier, NamedCurve namedCurve, Vector&& keyData, bool extractable, CryptoKeyUsageBitmap usages, bool* keyTypeMismatch) { - // FIXME: We should use the underlying crypto library to import PKCS8 OKP keys. - - // Read SEQUENCE - size_t index = 1; - if (keyData.size() < index + 1) + CBS cbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + EvpPKeyPtr key(EVP_parse_public_key(&cbs)); + if (!key || CBS_len(&cbs) != 0) { + ERR_clear_error(); return nullptr; - - // Read length and SEQUENCE - // FIXME: Check length is 5 + 1 + 1 + 1 + keyByteSize. - index += bytesUsedToEncodedLength(keyData[index]) + 1; - if (keyData.size() < index + 1) - return nullptr; - - // Read length - // FIXME: Check length is 5. - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 5) - return nullptr; - - // Read OID - // FIXME: spec says this is 1 3 101 11X but WPT tests expect 6 3 43 101 11X. - auto reportKeyTypeMismatch = [&] { - if (keyTypeMismatch && parsesAsSubjectPublicKeyInfo(keyData)) + } + if (EVP_PKEY_id(key.get()) != evpKeyType(namedCurve)) { + if (keyTypeMismatch) *keyTypeMismatch = true; - }; - if (keyData[index++] != 6 || keyData[index++] != 3 || keyData[index++] != 43 || keyData[index++] != 101) { - reportKeyTypeMismatch(); return nullptr; } - switch (namedCurve) { - case NamedCurve::X25519: - if (keyData[index++] != 110) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - case NamedCurve::Ed25519: - if (keyData[index++] != 112) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - }; - - // Read BIT STRING - if (keyData.size() < index + 2) - return nullptr; - if (keyData[index++] != 3) - return nullptr; - - // Read length - // FIXME: Check length is keyByteSize + 1. - index += bytesUsedToEncodedLength(keyData[index]); - - if (keyData.size() < index + 1) - return nullptr; - - // Initial octet - if (!!keyData[index]) + KeyMaterial publicKey(ED25519_PUBLIC_KEY_LEN); + size_t length = publicKey.size(); + if (!EVP_PKEY_get_raw_public_key(key.get(), publicKey.begin(), &length) || length != publicKey.size()) return nullptr; - ++index; - - return create(identifier, namedCurve, CryptoKeyType::Public, std::span { keyData.begin() + index, keyData.size() - index }, extractable, usages); + return create(identifier, namedCurve, CryptoKeyType::Public, WTF::move(publicKey), extractable, usages); } constexpr uint8_t OKPOIDFirstByte = 6; @@ -223,78 +164,25 @@ ExceptionOr> CryptoKeyOKP::exportSpki() const // For all of the OIDs, the parameters MUST be absent. RefPtr CryptoKeyOKP::importPkcs8(CryptoAlgorithmIdentifier identifier, NamedCurve namedCurve, Vector&& keyData, bool extractable, CryptoKeyUsageBitmap usages, bool* keyTypeMismatch) { - // FIXME: We should use the underlying crypto library to import PKCS8 OKP keys. - - // Read SEQUENCE - size_t index = 1; - if (keyData.size() < index + 1) + CBS cbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + EvpPKeyPtr key(EVP_parse_private_key(&cbs)); + if (!key || CBS_len(&cbs) != 0) { + ERR_clear_error(); return nullptr; - - // Read length - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) - return nullptr; - - // Read version - index += 3; - if (keyData.size() < index + 1) - return nullptr; - - // Read SEQUENCE - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) - return nullptr; - - // Read length - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 5) - return nullptr; - - // Read OID - auto reportKeyTypeMismatch = [&] { - if (keyTypeMismatch && parsesAsPrivateKeyInfo(keyData)) + } + if (EVP_PKEY_id(key.get()) != evpKeyType(namedCurve)) { + if (keyTypeMismatch) *keyTypeMismatch = true; - }; - if (keyData[index++] != OKPOIDFirstByte || keyData[index++] != OKPOIDSecondByte || keyData[index++] != OKPOIDThirdByte || keyData[index++] != OKPOIDFourthByte) { - reportKeyTypeMismatch(); return nullptr; } - switch (namedCurve) { - case NamedCurve::X25519: - if (keyData[index++] != OKPOIDX25519Byte) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - case NamedCurve::Ed25519: - if (keyData[index++] != OKPOIDEd25519Byte) { - reportKeyTypeMismatch(); - return nullptr; - } - break; - }; - - // Read OCTET STRING - if (keyData.size() < index + 2) - return nullptr; - - if (keyData[index++] != 4) - return nullptr; - - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 2) - return nullptr; - - // Read OCTET STRING - if (keyData[index++] != 4) + // The seed of an Ed25519 key, which is what create() takes. + KeyMaterial privateKey(X25519_PRIVATE_KEY_LEN); + size_t length = privateKey.size(); + if (!EVP_PKEY_get_raw_private_key(key.get(), privateKey.begin(), &length) || length != privateKey.size()) return nullptr; - - index += bytesUsedToEncodedLength(keyData[index]); - if (keyData.size() < index + 1) - return nullptr; - - return create(identifier, namedCurve, CryptoKeyType::Private, std::span { keyData.begin() + index, keyData.size() - index }, extractable, usages); + return create(identifier, namedCurve, CryptoKeyType::Private, WTF::move(privateKey), extractable, usages); } ExceptionOr> CryptoKeyOKP::exportPkcs8() const diff --git a/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.cpp b/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.cpp index d6bb6dfb1ab5..4c8bfe35309a 100644 --- a/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.cpp +++ b/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.cpp @@ -69,9 +69,8 @@ template<> JSString* convertEnumerationToJS(JSGlobalObject& lexicalGlobalObject, return jsStringWithCache(lexicalGlobalObject.vm(), convertEnumerationToString(enumerationValue)); } -template<> std::optional parseEnumeration(JSGlobalObject& lexicalGlobalObject, JSValue value) +template<> std::optional parseEnumerationFromString(const String& stringValue) { - auto stringValue = value.toWTFString(&lexicalGlobalObject); static constexpr SortedArrayMap enumerationMapping { std::to_array>({ { "decapsulateBits"_s, CryptoKeyUsage::DecapsulateBits }, { "decapsulateKey"_s, CryptoKeyUsage::DecapsulateKey }, @@ -91,6 +90,11 @@ template<> std::optional parseEnumeration(JSGlob return std::nullopt; } +template<> std::optional parseEnumeration(JSGlobalObject& lexicalGlobalObject, JSValue value) +{ + return parseEnumerationFromString(value.toWTFString(&lexicalGlobalObject)); +} + } // namespace WebCore #endif // ENABLE(WEB_CRYPTO) diff --git a/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.h b/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.h index 84d87e30d40e..f78ae192f7ee 100644 --- a/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.h +++ b/src/jsc/bindings/webcrypto/JSCryptoKeyUsage.h @@ -30,6 +30,7 @@ namespace WebCore { String convertEnumerationToString(CryptoKeyUsage); template<> JSC::JSString* convertEnumerationToJS(JSC::JSGlobalObject&, CryptoKeyUsage); +template<> std::optional parseEnumerationFromString(const String&); template<> std::optional parseEnumeration(JSC::JSGlobalObject&, JSC::JSValue); } // namespace WebCore diff --git a/src/jsc/bindings/webcrypto/JSJsonWebKey.cpp b/src/jsc/bindings/webcrypto/JSJsonWebKey.cpp index f5e9c8e187a2..2dbf3356c607 100644 --- a/src/jsc/bindings/webcrypto/JSJsonWebKey.cpp +++ b/src/jsc/bindings/webcrypto/JSJsonWebKey.cpp @@ -147,7 +147,19 @@ template<> JsonWebKey convertDictionary(JSGlobalObject& lexicalGloba RETURN_IF_EXCEPTION(throwScope, {}); } if (!key_opsValue.isUndefined()) { - result.key_ops = convert>>(lexicalGlobalObject, key_opsValue); + // A `sequence` in the specification. Like Node's validateKeyOps(), skip an operation that is + // not known, and reject a repeated one before the operations are compared with the usages. + auto operations = convert>(lexicalGlobalObject, key_opsValue); + RETURN_IF_EXCEPTION(throwScope, {}); + Vector keyOps; + HashSet seen; + for (auto& operation : operations) { + if (!seen.add(operation).isNewEntry) + result.hasDuplicateKeyOps = true; + else if (auto usage = parseEnumerationFromString(operation)) + keyOps.append(*usage); + } + result.key_ops = WTF::move(keyOps); RETURN_IF_EXCEPTION(throwScope, {}); } JSValue ktyValue; diff --git a/src/jsc/bindings/webcrypto/JsonWebKey.h b/src/jsc/bindings/webcrypto/JsonWebKey.h index 342dbe85b88f..ea2334d42af8 100644 --- a/src/jsc/bindings/webcrypto/JsonWebKey.h +++ b/src/jsc/bindings/webcrypto/JsonWebKey.h @@ -37,7 +37,10 @@ struct JsonWebKey { String kty; String use; // FIXME: Consider merging key_ops and usages. + // Without the operations that are not a CryptoKeyUsage, which are skipped. std::optional> key_ops; + // Counts those too. + bool hasDuplicateKeyOps { false }; CryptoKeyUsageBitmap usages; String alg; @@ -61,27 +64,10 @@ struct JsonWebKey { String priv; }; -// Node's validateKeyOps rejects a repeated key_ops entry for every algorithm -// before the operations/usages mismatch check. -inline bool hasDuplicateJwkKeyOps(const std::optional>& keyOps) -{ - if (!keyOps) - return false; - CryptoKeyUsageBitmap seenOps = 0; - for (auto op : *keyOps) { - // The binding enum order matches the bitmap bit order. - CryptoKeyUsageBitmap bit = 1 << static_cast(op); - if (seenOps & bit) - return true; - seenOps |= bit; - } - return false; -} - // Whether `key_ops`, if the key has any, is well formed and covers `usages`. inline bool jwkKeyOpsAllow(const JsonWebKey& jwk, CryptoKeyUsageBitmap usages) { - return !jwk.key_ops || (!hasDuplicateJwkKeyOps(jwk.key_ops) && (jwk.usages & usages) == usages); + return !jwk.key_ops || (!jwk.hasDuplicateKeyOps && (jwk.usages & usages) == usages); } } // namespace WebCore diff --git a/src/runtime/crypto/EVP.rs b/src/runtime/crypto/EVP.rs index 9f9e4228d53c..f1a8c46ad8fc 100644 --- a/src/runtime/crypto/EVP.rs +++ b/src/runtime/crypto/EVP.rs @@ -120,6 +120,8 @@ bun_core::comptime_string_map! { b"sha512-224" => Algorithm::Sha512_224, b"sha-512256" => Algorithm::Sha512_256, b"sha512-256" => Algorithm::Sha512_256, + b"blake2b-512" => Algorithm::Blake2b512, + b"blake2s-256" => Algorithm::Blake2s256, b"sha-512/224" => Algorithm::Sha512_224, b"sha-512_224" => Algorithm::Sha512_224, b"sha-512/256" => Algorithm::Sha512_256, diff --git a/src/runtime/crypto/PBKDF2.rs b/src/runtime/crypto/PBKDF2.rs index 77d02a76dee5..525c132a0956 100644 --- a/src/runtime/crypto/PBKDF2.rs +++ b/src/runtime/crypto/PBKDF2.rs @@ -5,29 +5,35 @@ use bun_jsc::{JSGlobalObject, JSValue, Job, JobContext, JsResult, JsThread, Stro use crate::node::{StringOrBuffer, ThreadIsolated}; -use crate::crypto::evp::{self, Algorithm}; - pub(crate) struct PBKDF2 { iteration_count: u32, pub length: usize, - algorithm: Algorithm, + digest: Digest, +} + +#[derive(Clone, Copy)] +pub(crate) struct Digest(core::ptr::NonNull); + +// SAFETY: BoringSSL's `EVP_MD`s are immutable statics. They hold no VM or JS state. +unsafe impl Send for Digest {} + +unsafe extern "C" { + /// The lookup of `createHmac()` and the rest of `node:crypto`. + fn Bun__getDigestByName(name: *const u8, length: usize) -> *const boringssl::EVP_MD; } -pub(crate) fn algorithm_from_digest( - global_this: &JSGlobalObject, - digest: &[u8], -) -> JsResult { - match evp::lookup_ignore_case(digest) { - Some(Algorithm::Shake128 | Algorithm::Shake256) | None => {} - Some(algorithm) if algorithm.md().is_none() => {} - Some(algorithm) => return Ok(algorithm), +pub(crate) fn digest_by_name(global_this: &JSGlobalObject, name: &[u8]) -> JsResult { + // SAFETY: `name` is valid for `name.len()` bytes, and the callee does not keep it. + let digest = unsafe { Bun__getDigestByName(name.as_ptr(), name.len()) }; + match core::ptr::NonNull::new(digest.cast_mut()) { + Some(digest) => Ok(Digest(digest)), + None => Err(global_this + .err( + bun_jsc::ErrorCode::CRYPTO_INVALID_DIGEST, + format_args!("Invalid digest: {}", bstr::BStr::new(name)), + ) + .throw()), } - Err(global_this - .err( - bun_jsc::ErrorCode::CRYPTO_INVALID_DIGEST, - format_args!("Invalid digest: {}", bstr::BStr::new(digest)), - ) - .throw()) } impl PBKDF2 { @@ -37,7 +43,7 @@ impl PBKDF2 { salt: &[u8], iteration_count: u32, length: usize, - algorithm: Algorithm, + digest: Digest, ) -> JsResult { if salt.len() > i32::MAX as usize { return Err(global_this.throw_invalid_arguments(format_args!("salt is too long"))); @@ -48,12 +54,11 @@ impl PBKDF2 { Ok(PBKDF2 { iteration_count, length, - algorithm, + digest, }) } pub(crate) fn run(&self, password: &[u8], salt: &[u8], output: &mut [u8]) -> bool { - let algorithm = self.algorithm; let iteration_count = self.iteration_count; let length = self.length; @@ -65,7 +70,7 @@ impl PBKDF2 { } boringssl::ERR_clear_error(); // SAFETY: password/salt point to valid slices for the given lengths; - // algorithm.md() returns a non-null EVP_MD; output is writable for `length` bytes. + // the digest is a non-null EVP_MD; output is writable for `length` bytes. let rc = unsafe { boringssl::PKCS5_PBKDF2_HMAC( if !password.is_empty() { @@ -77,7 +82,7 @@ impl PBKDF2 { salt.as_ptr(), salt.len(), iteration_count as c_uint, - algorithm.md().unwrap(), + self.digest.0.as_ptr(), length, output.as_mut_ptr(), ) diff --git a/src/runtime/node/node_crypto_binding.bind.ts b/src/runtime/node/node_crypto_binding.bind.ts index 17c408769f97..79b1e4ec8ef6 100644 --- a/src/runtime/node/node_crypto_binding.bind.ts +++ b/src/runtime/node/node_crypto_binding.bind.ts @@ -151,7 +151,6 @@ export const crypto = b.namespace({ getFips: { returns: b.i32 }, setFips: { length: 1 }, setEngine: { length: 2, capability: "heap", throws: true }, - getHashes: { returns: b.array(b.string.utf8) }, scrypt: [ { args: { ...passwordAndSaltHandles, keylen, options: ScryptOptions, callback: b.callable }, diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index e5903a6cb196..241a5297bd62 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -1,12 +1,9 @@ //! `node:crypto` native binding — `pbkdf2`/`scrypt`/`random*` //! plus the `ExternCryptoJob` / `CryptoJob` work-pool plumbing. -use core::ffi::{c_char, c_void}; - // `rust-argon2` exports its lib as crate `argon2`; alias past the `argon2` fn below. use ::argon2 as rust_argon2; use bun_boringssl as boringssl; -use bun_collections::CaseInsensitiveAsciiStringArrayHashMap; use bun_core::String as BunString; use bun_jsc::bindgen::{self, BufferSource, Callable, JsHeap, JsLocal, JsScope, StringOrJsBytes}; use bun_jsc::{ @@ -919,7 +916,7 @@ pub(crate) fn pbkdf2<'a>( JSValue::UNDEFINED, )); }; - let algorithm = pbkdf2::algorithm_from_digest(cx.global(), digest)?; + let digest = pbkdf2::digest_by_name(cx.global(), digest)?; let password = isolate_for_job(cx, password)?; let salt = isolate_for_job(cx, salt)?; @@ -929,7 +926,7 @@ pub(crate) fn pbkdf2<'a>( salt.slice(), iterations, keylen as usize, - algorithm, + digest, )?; pbkdf2::create_job(&cx.js_thread(), data, password, salt, callback.to_js()); Ok(()) @@ -959,14 +956,14 @@ pub(crate) fn pbkdf2_sync( ) -> JsResult { // SAFETY: it only allocates the result and throws, and nothing borrowed is read after a throw. let global_this = unsafe { heap.global() }; - let algorithm = pbkdf2::algorithm_from_digest(global_this, digest)?; + let digest = pbkdf2::digest_by_name(global_this, digest)?; let data = PBKDF2::new( global_this, password, salt, iterations, keylen as usize, - algorithm, + digest, )?; // `create_buffer_from_length` → `JSBuffer__bufferFromLength`, which constructs // with `JSBufferSubclassStructure` (a Node.js `Buffer`, not a plain Uint8Array/ArrayBuffer). @@ -1001,38 +998,6 @@ pub(crate) fn set_engine<'a>(heap: &JsHeap<'a>) -> bindgen::Result<'a, ()> { )) } -extern "C" fn for_each_hash( - _: *const boringssl::c::EVP_MD, - maybe_from: *const c_char, - _: *const c_char, - ctx: *mut c_void, -) { - if maybe_from.is_null() { - return; - } - // SAFETY: ctx was `&mut CaseInsensitiveAsciiStringArrayHashMap<()>` cast in `get_hashes`. - let hashes: &mut CaseInsensitiveAsciiStringArrayHashMap<()> = - unsafe { bun_ptr::callback_ctx::>(ctx) }; - // SAFETY: `maybe_from` is non-null (checked above) and points to a NUL-terminated C string - // from BoringSSL's static tables. - let from_bytes = unsafe { bun_core::ffi::cstr(maybe_from) }.to_bytes(); - bun_core::handle_oom(hashes.put(from_bytes, ())); -} - -pub(crate) fn get_hashes() -> Vec> { - let mut hashes: CaseInsensitiveAsciiStringArrayHashMap<()> = - CaseInsensitiveAsciiStringArrayHashMap::new(); - - // Perf idea (dylan-conway): cache the names - // SAFETY: `for_each_hash` matches the expected callback signature; `&mut hashes` is valid - // for the duration of the call. - unsafe { - boringssl::c::EVP_MD_do_all_sorted(for_each_hash, (&raw mut hashes).cast::()); - } - - core::mem::take(&mut *hashes).into_entries().0 -} - pub(crate) fn scrypt<'a>( cx: &mut JsScope<'a>, password: StringOrJsBytes<'a>, diff --git a/test/aot/built-ins-alone/fresh-results-calls.ts b/test/aot/built-ins-alone/fresh-results-calls.ts index df92162d44e7..1e68efed98a4 100644 --- a/test/aot/built-ins-alone/fresh-results-calls.ts +++ b/test/aot/built-ins-alone/fresh-results-calls.ts @@ -258,7 +258,6 @@ export const calls: Record = { "%CrashHandlerJscCrashHandler%.getFeatureData": [ 'import { crash_handler } from "bun:internal-for-testing";\ncrash_handler.getFeatureData()', ], - "%NodeCryptoBindingCrypto%.getHashes": ['import { getHashes } from "node:crypto";\ngetHashes()'], "%Generator%.prototype.throw": [ "function* numbers() {\n try {\n yield 1;\n } catch {\n yield 2;\n }\n}\nconst generator = numbers();\ngenerator.next();\ngenerator.throw(1)", ], diff --git a/test/internal/source-lints/vm-thread-door.inventory.json b/test/internal/source-lints/vm-thread-door.inventory.json index 939fe950c6d5..7f0babbf8b32 100644 --- a/test/internal/source-lints/vm-thread-door.inventory.json +++ b/test/internal/source-lints/vm-thread-door.inventory.json @@ -124,6 +124,11 @@ "SharedWatcher" ] }, + "src/runtime/crypto/PBKDF2.rs": { + "unsafe impl Send": [ + "Digest" + ] + }, "src/runtime/dns_jsc/dns.rs": { "WorkPool::schedule*": 1, "unsafe impl Send": [ diff --git a/test/js/node/crypto/crypto-oneshot.test.ts b/test/js/node/crypto/crypto-oneshot.test.ts index f53376f8d39d..9eb9cdb97ff8 100644 --- a/test/js/node/crypto/crypto-oneshot.test.ts +++ b/test/js/node/crypto/crypto-oneshot.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import crypto from "crypto"; import { readFileSync } from "fs"; +import { bunEnv, bunExe } from "harness"; import { path } from "../test/common/fixtures"; describe("crypto.hash", () => { @@ -31,7 +32,7 @@ describe("crypto.hash", () => { ); }); - [null, true, 1, () => {}, {}].forEach(invalid => { + [null, true, 1, () => {}, NaN, Symbol(0)].forEach(invalid => { // @ts-expect-error expect(() => crypto.hash("sha1", "test", invalid)).toThrow( expect.objectContaining({ @@ -47,6 +48,182 @@ describe("crypto.hash", () => { }), ); }); + + test("says what is wrong like Node", () => { + const hash = crypto.hash as (...args: unknown[]) => unknown; + expect(hash).toHaveLength(3); + expect(() => hash("nope", "x")).toThrow( + expect.objectContaining({ name: "Error", message: "Digest method nope is not supported" }), + ); + expect(() => hash("sha256", [97])).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: + 'The "input" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Array', + }), + ); + expect(() => hash("sha256", "x", "nope")).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_VALUE", + message: "The argument 'outputEncoding' is invalid. Received 'nope'", + }), + ); + expect(() => hash("sha256", "x", { outputEncoding: 5 })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" }), + ); + }); + + test("checks its input itself", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `ArrayBuffer.isView = () => true; + const { hash } = require("node:crypto"); + for (const input of [{}, 5, [], () => {}, null]) { + for (const options of [undefined, { outputLength: 0 }]) { + try { + console.log(hash(options ? "shake128" : "sha256", input, options)); + } catch (e) { + console.log(e.code); + } + } + } + // What is replaced later is not asked. + ArrayBuffer.isView = () => false; + String.prototype.toLowerCase = () => "buffer"; + console.log(hash("sha1", Buffer.from("abc"))); + try { + hash("sha1", "abc", "nope"); + } catch (e) { + console.log(e.code); + }`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "ERR_INVALID_ARG_TYPE\n".repeat(10) + "a9993e364706816aba3e25717850c26c9cd0d89d\nERR_INVALID_ARG_VALUE\n", + stderr: "", + exitCode: 0, + }); + }); + + test("takes what Bun.CryptoHasher.hash() takes", () => { + const hash = crypto.hash as (...args: unknown[]) => unknown; + const shared = new SharedArrayBuffer(3); + new Uint8Array(shared).set([97, 98, 99]); + for (const input of [ + new TextEncoder().encode("abc").buffer, + shared, + new Blob(["a", "bc"]), + new File(["abc"], "name"), + new String("abc"), + new DataView(new TextEncoder().encode("abc").buffer), + ]) { + expect(hash("sha1", input)).toBe("a9993e364706816aba3e25717850c26c9cd0d89d"); + expect(hash("RSA-SHA1", input, { outputEncoding: "base64" })).toBe("qZk+NkcGgWq6PiVxeFDCbJzQ2J0="); + expect(hash("shake128", input, { outputLength: 4 })).toBe("5881092d"); + } + for (const input of [new ArrayBuffer(0), new Blob([]), new String("")]) { + expect(hash("sha1", input)).toBe("da39a3ee5e6b4b0d3255bfef95601890afd80709"); + } + expect(() => hash("sha1", Bun.file(import.meta.path))).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE", message: "File blob cannot be used here" }), + ); + }); + + test("takes an options object", () => { + const digest = crypto.createHash("sha256").update("x").digest(); + expect(crypto.hash("sha256", "x", {})).toBe(digest.toString("hex")); + expect(crypto.hash("sha256", "x", { outputEncoding: "base64" })).toBe(digest.toString("base64")); + expect(crypto.hash("sha256", "x", { outputEncoding: "buffer" })).toEqual(digest); + expect(crypto.hash("sha256", "x", { outputLength: 32 })).toBe(digest.toString("hex")); + }); + + test("writes the digest into a TypedArray or a DataView, like Bun.CryptoHasher.hash()", () => { + const hash = crypto.hash as (...args: unknown[]) => unknown; + const digest = crypto.createHash("sha256").update("x").digest(); + for (const output of [ + new Uint8Array(32), + Buffer.alloc(32), + new Uint32Array(8), + new DataView(new ArrayBuffer(32)), + new Uint8Array(40), + ]) { + expect(hash("sha256", "x", output)).toBe(output); + expect(Buffer.from(output.buffer, output.byteOffset, 32)).toEqual(digest); + } + expect(() => hash("sha256", "x", new Uint8Array(31))).toThrow("TypedArray must be at least 32 bytes"); + }); + + test("outputLength", () => { + const long = crypto.hash("shake128", "x", { outputLength: 100 }); + expect(crypto.hash("sha256", long)).toBe("7bcaed54dcf612d4659d3f9635c9cc8eb472abf78c9b1214f94efb95b53686e5"); + for (const outputLength of [1, 5, 16, 64, 65]) { + expect(crypto.hash("shake128", "x", { outputLength })).toBe(long.slice(0, outputLength * 2)); + expect(crypto.hash("shake128", "x", { outputLength, outputEncoding: "buffer" })).toEqual( + crypto.createHash("shake128", { outputLength }).update("x").digest(), + ); + } + expect(crypto.hash("shake256", "x", { outputLength: 1 })).toBe("c8"); + expect(crypto.hash("shake256", "x", { outputLength: 0 })).toBe(""); + expect(crypto.hash("shake256", "x", { outputLength: 0, outputEncoding: "buffer" })).toEqual(Buffer.alloc(0)); + + for (const algorithm of ["sha256", "blake2s256"]) { + expect(() => crypto.hash(algorithm, "x", { outputLength: 5 })).toThrow( + expect.objectContaining({ + name: "Error", + message: `Output length 5 is invalid for ${algorithm}, which does not support XOF`, + }), + ); + } + expect(() => crypto.hash("shake128", "x", { outputLength: -1 })).toThrow( + expect.objectContaining({ code: "ERR_OUT_OF_RANGE" }), + ); + expect(() => crypto.hash("shake128", "x", { outputLength: "5" as any })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_TYPE" }), + ); + }); + + test.concurrent.each([ + ['crypto.hash("shake128", "x")', 1], + ['crypto.hash("SHAKE-256", "x", "hex")', 1], + ['crypto.hash("shake256", "x", {})', 1], + ['crypto.createHash("shake128")', 1], + ['crypto.createHash("shake128"); crypto.hash("shake256", "x"); crypto.createHash("shake256", {})', 1], + [ + 'crypto.hash("shake128", "x", { outputLength: 16 }); crypto.createHash("shake256", { outputLength: 32 }).copy()', + 0, + ], + ['crypto.hash("sha256", "x"); crypto.createHash("sha3-256")', 0], + ])("%s warns %d times that SHAKE wants an outputLength", async (script, times) => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `const crypto = require("node:crypto"); ${script};`], + env: bunEnv, + stderr: "pipe", + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + expect( + stderr.split( + "[DEP0198] DeprecationWarning: Creating SHAKE128/256 digests without an explicit options.outputLength is deprecated.", + ), + ).toHaveLength(times + 1); + expect(exitCode).toBe(0); + }); + + test("does not drop the options of cSHAKE", () => { + for (const options of [{ customization: "a" }, { functionName: "a" }]) { + expect(() => crypto.hash("shake128", "x", { outputLength: 8, ...options } as any)).toThrow( + "Digest options are not supported", + ); + expect(() => crypto.createHash("shake128", { outputLength: 8, ...options } as any)).toThrow( + "Digest method not supported", + ); + } + }); + const input = readFileSync(path("utf8_test_text.txt")); [ "blake2b256", diff --git a/test/js/node/crypto/crypto-rsa.test.js b/test/js/node/crypto/crypto-rsa.test.js index 2632b36d9823..947807be7abd 100644 --- a/test/js/node/crypto/crypto-rsa.test.js +++ b/test/js/node/crypto/crypto-rsa.test.js @@ -364,6 +364,48 @@ test("the OAEP options under another padding", () => { } }); +describe("RSA PKCS#1 v1.5 signatures", () => { + const data = Buffer.from("data"); + + // These signatures are deterministic. Each fingerprint is the SHA-256 of the one Node makes. + test.each([ + ["sha512-224", "0c742fc54f244df03bd3a1276a3ec8ee5d8bb338ff5ab8b281735843a867bbd6"], + ["sha512-256", "4000024a0e99559a2948ddd8a97d0c2d5bd12cff7c68f999d47912e31f46a69d"], + ["sha3-224", "1cc8289800225af9af529fb5e6f83a341931a4620d182cb986f81eb831e974ac"], + ["sha3-256", "68a3c81111e4c9c36b79f6092f631808b448afacaa386f9911d91ba50aa53519"], + ["sha3-384", "17260672f13c4cc8fc00dada4bb2fecb24e088dc670f51695a4f416e6e508139"], + ["sha3-512", "a83acf60398ef4079e9afc4c0bbf4deb82a931acf68ea55b2a985da490b543f0"], + ["ripemd160", "b79ab8f388a91288b2effc784863f170b365e5b9d9451846b2a6bbdd03707f6b"], + ])("with %s", (digest, fingerprint) => { + const signature = crypto.sign(digest, data, rsaKeyPem); + expect(crypto.hash("sha256", signature)).toBe(fingerprint); + expect(crypto.createSign(digest).update(data).sign(rsaKeyPem)).toEqual(signature); + expect(crypto.verify(digest, data, rsaPubPem, signature)).toBe(true); + expect(crypto.createVerify(digest).update(data).verify(rsaPubPem, signature)).toBe(true); + expect(crypto.verify(digest, Buffer.from("other"), rsaPubPem, signature)).toBe(false); + expect(crypto.createVerify(digest).update("other").verify(rsaPubPem, signature)).toBe(false); + }); + + test.each(["SHA3-256", "SHA3-384", "SHA3-512"])("with %s in WebCrypto", async hash => { + const algorithm = { name: "RSASSA-PKCS1-v1_5", hash }; + const privateKey = crypto.createPrivateKey(rsaKeyPem).toCryptoKey(algorithm, false, ["sign"]); + const publicKey = crypto.createPublicKey(rsaPubPem).toCryptoKey(algorithm, false, ["verify"]); + const signature = await crypto.subtle.sign(algorithm, privateKey, data); + expect(Buffer.from(signature)).toEqual(crypto.sign(hash.toLowerCase(), data, rsaKeyPem)); + expect(await crypto.subtle.verify(algorithm, publicKey, signature, data)).toBe(true); + expect(await crypto.subtle.verify(algorithm, publicKey, signature, Buffer.from("other"))).toBe(false); + }); +}); + +test.each(["hash", "mgf1Hash"])("rsa-pss keys are no longer generated with options.%s", option => { + expect(() => crypto.generateKeyPairSync("rsa-pss", { modulusLength: 2048, [option]: "sha256" })).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_VALUE", + message: `The property 'options.${option}' is no longer supported. Received 'sha256'`, + }), + ); +}); + test("RSA-OAEP test vectors", () => { const { decryptionTests } = JSON.parse(fixtures.readSync("rsa-oaep-test-vectors.js", "utf8")); diff --git a/test/js/node/crypto/crypto.key-objects.test.ts b/test/js/node/crypto/crypto.key-objects.test.ts index f853fb66cf6e..976d7b3470bf 100644 --- a/test/js/node/crypto/crypto.key-objects.test.ts +++ b/test/js/node/crypto/crypto.key-objects.test.ts @@ -679,6 +679,95 @@ describe("crypto.KeyObjects", () => { ); }); + test("an EC jwk needs strings for its members, a point on its curve, and the d that gives that point", () => { + const jwk = generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey.export({ format: "jwk" }); + const other = generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey.export({ format: "jwk" }); + const { kty, crv, x, y } = jwk; + expect(createPrivateKey({ key: jwk, format: "jwk" }).export({ format: "jwk" })).toEqual(jwk); + const publicKey = createPublicKey({ key: jwk, format: "jwk" }); + expect(publicKey.type).toBe("public"); + expect(publicKey.export({ format: "jwk" })).toEqual({ kty, crv, x, y }); + + const invalid = expect.objectContaining({ code: "ERR_CRYPTO_INVALID_JWK", message: "Invalid JWK EC key" }); + const wrongMembers = [ + { d: other.d }, + { d: "" }, + { d: 5 }, + { d: null }, + { x: other.x }, + { x: 5 }, + { x: undefined }, + { y: undefined }, + { crv: 5 }, + { crv: undefined }, + ]; + for (const wrong of wrongMembers) { + for (const create of [createPrivateKey, createPublicKey]) { + expect(() => create({ key: { ...jwk, ...wrong } as JsonWebKey, format: "jwk" })).toThrow(invalid); + } + } + expect(() => createPublicKey({ key: { ...jwk, crv: "nope" }, format: "jwk" })).toThrow( + expect.objectContaining({ code: "ERR_CRYPTO_INVALID_CURVE", message: "Invalid EC curve name" }), + ); + // The whole jwk is read before its kind is looked at. + expect(() => createPrivateKey({ key: { kty, crv, x: other.x, y }, format: "jwk" })).toThrow(invalid); + expect(() => createPrivateKey({ key: { kty, crv, x, y }, format: "jwk" })).toThrow( + expect.objectContaining({ code: "ERR_CRYPTO_INVALID_JWK", message: "JWK does not contain private key material" }), + ); + }); + + test('type: "sec1" takes what it took before', () => { + const rsa = generateKeyPairSync("rsa", { modulusLength: 2048 }).privateKey; + const ed25519 = generateKeyPairSync("ed25519").privateKey; + const ec = generateKeyPairSync("ec", { namedCurve: "P-256" }).privateKey; + for (const [key, type] of [ + [rsa, "pkcs1"], + [rsa, "pkcs8"], + [ed25519, "pkcs8"], + [ec, "pkcs8"], + [ec, "sec1"], + ] as const) { + const der = key.export({ type: type as "pkcs8", format: "der" }); + expect(createPrivateKey({ key: der, format: "der", type: "sec1" }).equals(key)).toBe(true); + } + }); + + test("an ECPrivateKey in DER may leave out its public key", () => { + const { privateKey, publicKey } = generateKeyPairSync("ec", { namedCurve: "P-256" }); + const d = Buffer.from(privateKey.export({ format: "jwk" }).d!, "base64url"); + // SEQUENCE { INTEGER 1, OCTET STRING d, [0] { OID prime256v1 } } + const body = Buffer.concat([Buffer.from("0201010420", "hex"), d, Buffer.from("a00a06082a8648ce3d030107", "hex")]); + const der = Buffer.concat([Buffer.from([0x30, body.length]), body]); + const imported = createPrivateKey({ key: der, format: "der", type: "sec1" }); + expect(imported.equals(privateKey)).toBe(true); + expect(createPublicKey(imported).equals(publicKey)).toBe(true); + }); + + test("the encoding of a key given as a string", () => { + const unknown = expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING", message: "Unknown encoding: nope" }); + expect(() => createSecretKey("abcd", "nope" as BufferEncoding)).toThrow(unknown); + expect(() => createPublicKey({ key: publicPem, encoding: "nope" as BufferEncoding })).toThrow(unknown); + expect(() => createPrivateKey({ key: privatePem, encoding: "nope" as BufferEncoding })).toThrow(unknown); + + expect(createSecretKey("abcd", "buffer" as BufferEncoding).export()).toEqual(Buffer.from("abcd")); + expect(createSecretKey("abcd", "hex").export()).toEqual(Buffer.from([0xab, 0xcd])); + expect(createSecretKey("abc", "hex").export()).toEqual(Buffer.from([0xab])); + const hex = Buffer.from(privatePem).toString("hex"); + expect(createPrivateKey({ key: hex, encoding: "hex" }).equals(createPrivateKey(privatePem))).toBe(true); + }); + + test.each(["aes128", "aes192", "aes256", "des3"])("a private key is exported with the cipher %s", cipher => { + const privateKey = createPrivateKey(privatePem); + const pem = privateKey.export({ type: "pkcs8", format: "pem", cipher, passphrase: "secret" }); + expect(createPrivateKey({ key: pem, passphrase: "secret" }).equals(privateKey)).toBe(true); + }); + + test("a PEM may start with a byte order mark", () => { + expect(createPrivateKey("\ufeff" + privatePem).equals(createPrivateKey(privatePem))).toBe(true); + expect(createPublicKey("\ufeff" + publicPem).equals(createPublicKey(publicPem))).toBe(true); + expect(createPublicKey("\ufeff" + privatePem).equals(createPublicKey(publicPem))).toBe(true); + }); + test("private encrypted should work", async () => { // Reading an encrypted key without a passphrase should fail. expect(() => createPrivateKey(privateEncryptedPem)).toThrow(); @@ -861,31 +950,10 @@ describe("crypto.KeyObjects", () => { } }); - it("rsa-pss deprecated hash/mgf1Hash options: an error thrown while emitting the DEP0154 warning propagates", async () => { - // Emitting a DeprecationWarning reads process.noDeprecation; run in a child so the getter can't - // affect other tests. - await using proc = Bun.spawn({ - cmd: [ - bunExe(), - "-e", - `Object.defineProperty(process, "noDeprecation", { get() { throw new Error("noDeprecation getter"); } }); - for (const opt of ["hash", "mgf1Hash"]) { - try { - require("crypto").generateKeyPairSync("rsa-pss", { modulusLength: 512, [opt]: "sha256" }); - console.log(opt + ": no throw"); - } catch (e) { - console.log(opt + ": " + e.message); - } - }`, - ], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe("hash: noDeprecation getter\nmgf1Hash: noDeprecation getter\n"); - expect(stderr).toBe(""); - expect(exitCode).toBe(0); + it.each(["ed25519", "x25519"] as const)("a JWK of an %s key has its members in Node's order", keyType => { + const { publicKey, privateKey } = generateKeyPairSync(keyType as "ed25519"); + expect(Object.keys(publicKey.export({ format: "jwk" }))).toEqual(["kty", "crv", "x"]); + expect(Object.keys(privateKey.export({ format: "jwk" }))).toEqual(["kty", "crv", "x", "d"]); }); ["ed25519", "x25519"].forEach(keyType => { diff --git a/test/js/node/crypto/crypto.test.ts b/test/js/node/crypto/crypto.test.ts index b164a672cd39..ad400d34ddca 100644 --- a/test/js/node/crypto/crypto.test.ts +++ b/test/js/node/crypto/crypto.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it } from "bun:test"; import crypto from "crypto"; import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness"; import path from "path"; +import { readKey } from "../test/common/fixtures"; import { hashesFixture } from "./fixtures/sign.fixture.ts"; const HashClasses = [MD5, MD4, SHA1, SHA224, SHA256, SHA384, SHA512, SHA512_256]; @@ -226,6 +227,120 @@ describe("crypto.getCurves", () => { expect(Array.isArray(crypto.getCurves())).toBe(true); expect(typeof crypto.getCurves()[0]).toBe("string"); }); + + it("is sorted", () => { + expect(crypto.getCurves()).toEqual(crypto.getCurves().toSorted()); + }); +}); + +describe("crypto.getCipherInfo", () => { + it.each([ + ["aes-128-cbc", { mode: "cbc", name: "aes-128-cbc", nid: 419, keyLength: 16, blockSize: 16, ivLength: 16 }], + ["aes-128-ecb", { mode: "ecb", name: "aes-128-ecb", nid: 418, keyLength: 16, blockSize: 16, ivLength: undefined }], + ["rc4", { mode: "stream", name: "rc4", nid: 5, keyLength: 16, blockSize: undefined, ivLength: undefined }], + ])("of %s has the properties of Node's, in their order", (name, expected) => { + const info = crypto.getCipherInfo(name)!; + expect(Object.entries(info)).toEqual(Object.entries(expected)); + expect(Object.getPrototypeOf(info)).toBeNull(); + }); +}); + +it("crypto.getRandomValues has its name", () => { + expect(crypto.getRandomValues.name).toBe("getRandomValues"); + expect(crypto.getRandomValues).toHaveLength(1); +}); + +describe("names of digests", () => { + const rsaKey = readKey("rsa_private.pem", "ascii"); + // Every function that takes the name of a digest. The last two are for digests that RSA signs with. + const functions: [string, (name: string) => unknown][] = [ + ["createHash", name => crypto.createHash(name).update("x").digest("hex")], + ["hash", name => crypto.hash(name, "x")], + ["createHmac", name => crypto.createHmac(name, "key").update("x").digest("hex")], + ["pbkdf2Sync", name => crypto.pbkdf2Sync("password", "salt", 1, 8, name).toString("hex")], + ["hkdfSync", name => Buffer.from(crypto.hkdfSync(name, "key", "salt", "info", 8)).toString("hex")], + ["sign", name => crypto.sign(name, Buffer.from("x"), rsaKey).toString("hex")], + ["createSign", name => crypto.createSign(name).update("x").sign(rsaKey, "hex")], + ]; + + describe.each([ + ["sha256", ["Sha256", "RSA-SHA256", "rsa-sha256", "sha2-256", "SHA-256", "sha256withrsaencryption"]], + ["sha1", ["Sha1", "RSA-SHA1-2", "ssl3-sha1", "SHA1WITHRSAENCRYPTION"]], + ["md5", ["RSA-MD5", "rsa-md5", "ssl3-md5", "Md5WithRsaEncryption"]], + ["ripemd160", ["Ripemd160", "ripemd", "ripemd-160", "rmd160", "RMD160", "RSA-RIPEMD160", "ripemd160WithRSA"]], + ["sha512-256", ["Sha512-256", "sha2-512/256", "RSA-SHA512/256", "sha512-256WithRSAEncryption"]], + ["sha3-256", ["Sha3-256", "RSA-SHA3-256", "id-rsassa-pkcs1-v1_5-with-sha3-256"]], + ["md5-sha1", ["MD5-SHA1", "Md5-Sha1"]], + ])("of %s", (canonical, aliases) => { + it.each(functions)("in %s()", (_, call) => { + const expected = call(canonical); + expect(aliases.map(alias => [alias, call(alias)])).toEqual(aliases.map(alias => [alias, expected])); + }); + }); + + it("of blake2b512, which HMAC takes too", () => { + for (const name of ["blake2b512", "BLAKE2b512", "blake2b-512"]) { + expect(crypto.createHmac(name, "k").update("x").digest("hex")).toBe( + "3d24b96bcbfb1458d029341f06c1be7861e8cc0408e507413ba4a59d74119164190d22eceb5060c276c23328ac181ddecde3d0c1a31d7d8ba81b59aaeae9e21e", + ); + expect(Buffer.from(crypto.hkdfSync(name, "k", "s", "i", 16)).toString("hex")).toBe( + "ba92a70abe7a73fe77cb4c9c6beea370", + ); + expect(crypto.createHash(name).update("x").digest("hex")).toBe(crypto.hash("blake2b512", "x")); + } + }); + + it("that only Bun has", () => { + for (const [name, canonical] of [ + ["sha128", "sha1"], + ["sha-512_224", "sha512-224"], + ["sha-512224", "sha512-224"], + ["sha-512_256", "sha512-256"], + ["sha-512256", "sha512-256"], + ]) { + for (const [, call] of functions) expect(call(name)).toBe(call(canonical)); + } + expect(crypto.pbkdf2Sync("password", "salt", 1, 8, "blake2b256").toString("hex")).toBe("2ae662d8acc3fd98"); + }); + + it.each(functions)("that %s() does not know", (label, call) => { + for (const name of ["nope", "", "sha256 ", "sha-257", "rsa-sha", "sha256WithRSA", "\u017fha256"]) { + expect(() => call(name)).toThrow( + label === "createHash" + ? expect.objectContaining({ name: "Error", message: "Digest method not supported" }) + : label === "hash" + ? expect.objectContaining({ name: "Error", message: `Digest method ${name} is not supported` }) + : expect.objectContaining({ name: "TypeError", code: "ERR_CRYPTO_INVALID_DIGEST" }), + ); + } + }); + + it("are all in getHashes()", () => { + const hashes = crypto.getHashes(); + expect(hashes).toEqual([...new Set(hashes)].toSorted()); + expect(hashes).toEqual( + expect.arrayContaining([ + "RSA-SHA256", + "blake2b512", + "md5-sha1", + "rmd160", + "sha-256", + "sha256", + "sha512-256", + "ssl3-md5", + ]), + ); + for (const name of hashes) { + const digest = crypto.createHash(name).update("x").digest("hex"); + expect([name, crypto.hash(name, "x")]).toEqual([name, digest]); + expect([name, crypto.hash(name.toUpperCase(), "x")]).toEqual([name, digest]); + // Node's tests count on these two for every name in the list. + expect([name, crypto.pbkdf2Sync("password", "salt", 1, 8, name)]).toEqual([name, expect.any(Buffer)]); + expect([name, crypto.hkdfSync(name, "key", "salt", "info", 8)]).toEqual([name, expect.any(ArrayBuffer)]); + } + hashes.length = 0; + expect(crypto.getHashes()).not.toBeEmpty(); + }); }); describe("crypto", () => { diff --git a/test/js/node/crypto/ecdh.test.ts b/test/js/node/crypto/ecdh.test.ts index 6feba55d2e4b..5624725dc36d 100644 --- a/test/js/node/crypto/ecdh.test.ts +++ b/test/js/node/crypto/ecdh.test.ts @@ -215,8 +215,12 @@ test("ECDH.convertKey - supports different input and output encodings", () => { 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'"); + ECDH.convertKey("0102030405", "prime256v1", "hex", "hex", "compressed"); + }).toThrow("Failed to convert Buffer to EC_POINT"); + + // No hex digits are no bytes, and like Node the answer to an empty key is an empty string. + expect(ECDH.convertKey("invalid-key", "prime256v1", "hex", "hex", "compressed")).toBe(""); + expect(ECDH.convertKey(Buffer.alloc(0), "prime256v1")).toBe(""); // Invalid curve expect(() => { diff --git a/test/js/node/crypto/node-crypto.test.js b/test/js/node/crypto/node-crypto.test.js index d5eae9b6b6bd..52cdd4b42c9f 100644 --- a/test/js/node/crypto/node-crypto.test.js +++ b/test/js/node/crypto/node-crypto.test.js @@ -260,36 +260,7 @@ describe("createHash", () => { }, }; - const unsupported = [ - "id-rsassa-pkcs1-v1_5-with-sha3-224", - "id-rsassa-pkcs1-v1_5-with-sha3-256", - "id-rsassa-pkcs1-v1_5-with-sha3-384", - "id-rsassa-pkcs1-v1_5-with-sha3-512", - "md5withrsaencryption", - "ripemd", - "ripemd160withrsa", - "rsa-md5", - "rsa-ripemd160", - "rsa-sha1-2", - "rsa-sha3-224", - "rsa-sha3-256", - "rsa-sha3-384", - "rsa-sha3-512", - "rsa-sha512/224", - "rsa-sha512/256", - "rsa-sm3", - "sha1withrsaencryption", - "sha224withrsaencryption", - "sha256withrsaencryption", - "sha384withrsaencryption", - "sha512withrsaencryption", - "sha512-224withrsaencryption", - "sha512-256withrsaencryption", - "sm3", - "sm3withrsaencryption", - "ssl3-md5", - "ssl3-sha1", - ]; + const unsupported = ["rsa-sm3", "sm3", "sm3withrsaencryption"]; for (const name_ in nodeValues) { const name = name_.toLowerCase(); @@ -552,7 +523,7 @@ describe("createHash", () => { } console.log(JSON.stringify(out)); `; - await using proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stderr: "pipe" }); + await using proc = Bun.spawn({ cmd: [bunExe(), "--no-deprecation", "-e", script], env: bunEnv, stderr: "pipe" }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout: stdout && JSON.parse(stdout), stderr, exitCode }).toEqual({ stdout: Object.fromEntries( @@ -1004,12 +975,14 @@ it("Cipheriv.update throws expected error for invalid data", () => { ); }); -it("verifyOneShot should not accept strings for signatures", () => { +it("verifyOneShot takes a string for the signature, like Node", () => { const data = Buffer.alloc(1); + const { publicKey } = crypto.generateKeyPairSync("ed25519"); + expect(crypto.verify(null, data, publicKey, "oops")).toBe(false); expect(() => { - crypto.verify(null, data, "test", "oops"); + crypto.verify(null, data, publicKey, 5); }).toThrow( - "The \"signature\" argument must be an instance of Buffer, TypedArray, or DataView. Received type string ('oops')", + 'The "signature" argument must be of type string or an instance of ArrayBuffer, Buffer, TypedArray, or DataView. Received type number (5)', ); }); @@ -1379,8 +1352,13 @@ describe("KeyObject raw-public / raw-private / raw-seed formats", () => { expect(pub2.export({ format: "raw-public" })).toEqual(rawPub); expect(priv2.export({ format: "raw-private" })).toEqual(rawPriv); - // A private key's raw-public export should match the public key's. - expect(privateKey.export({ format: "raw-public" })).toEqual(rawPub); + // Like Node, a private key has no raw-public format. + expect(() => privateKey.export({ format: "raw-public" })).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_VALUE", + message: "The property 'options.format' is invalid. Received 'raw-public'", + }), + ); }); if (keyType === "ec") { @@ -1415,11 +1393,14 @@ describe("KeyObject raw-public / raw-private / raw-seed formats", () => { it("raw key formats reject unsupported combinations", () => { const { publicKey: rsaPub, privateKey: rsaPriv } = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 }); - for (const fmt of ["raw-public", "raw-private", "raw-seed"]) { + for (const fmt of ["raw-private", "raw-seed"]) { expect(() => rsaPriv.export({ format: fmt })).toThrow( expect.objectContaining({ code: "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS" }), ); } + expect(() => rsaPriv.export({ format: "raw-public" })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }), + ); expect(() => rsaPub.export({ format: "raw-public" })).toThrow( expect.objectContaining({ code: "ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS" }), ); @@ -1804,3 +1785,398 @@ describe("the BoringSSL error queue", () => { }); }); }); + +describe.each([ + ["an ArrayBuffer", bytes => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)], + [ + "a SharedArrayBuffer", + bytes => { + const shared = new SharedArrayBuffer(bytes.length); + new Uint8Array(shared).set(bytes); + return shared; + }, + ], +])("%s is taken", (_, wrap) => { + it("by DiffieHellman", () => { + const peer = crypto.createDiffieHellman(64); + peer.generateKeys(); + const dh = crypto.createDiffieHellman(wrap(peer.getPrime()), wrap(peer.getGenerator())); + expect([dh.getPrime(), dh.getGenerator()]).toEqual([peer.getPrime(), peer.getGenerator()]); + dh.generateKeys(); + expect(dh.computeSecret(wrap(peer.getPublicKey()))).toEqual(peer.computeSecret(dh.getPublicKey())); + dh.setPublicKey(wrap(peer.getPublicKey())); + dh.setPrivateKey(wrap(peer.getPrivateKey())); + expect([dh.getPublicKey(), dh.getPrivateKey()]).toEqual([peer.getPublicKey(), peer.getPrivateKey()]); + }); + + it("by ECDH", () => { + const peer = crypto.createECDH("prime256v1"); + peer.generateKeys(); + const ecdh = crypto.createECDH("prime256v1"); + ecdh.generateKeys(); + expect(ecdh.computeSecret(wrap(peer.getPublicKey()))).toEqual(peer.computeSecret(ecdh.getPublicKey())); + ecdh.setPrivateKey(wrap(peer.getPrivateKey())); + expect(ecdh.getPublicKey()).toEqual(peer.getPublicKey()); + expect(crypto.ECDH.convertKey(wrap(peer.getPublicKey()), "prime256v1", undefined, undefined, "compressed")).toEqual( + peer.getPublicKey(null, "compressed"), + ); + }); + + it("by generatePrime and checkPrime", () => { + const prime = crypto.generatePrimeSync(32, { + add: wrap(Buffer.from([8])), + rem: wrap(Buffer.from([3])), + bigint: true, + }); + expect(prime % 8n).toBe(3n); + expect(crypto.checkPrimeSync(wrap(Buffer.from([7])))).toBe(true); + }); + + it("by a cipher, except for update()", () => { + const [key, iv, aad, data] = [Buffer.alloc(16, 1), Buffer.alloc(12, 2), Buffer.alloc(4, 3), Buffer.alloc(8, 4)]; + const expected = crypto.createCipheriv("aes-128-gcm", key, iv).setAAD(aad); + const cipher = crypto.createCipheriv("aes-128-gcm", wrap(key), wrap(iv)).setAAD(wrap(aad)); + const encrypted = Buffer.concat([cipher.update(data), cipher.final()]); + expect(encrypted).toEqual(Buffer.concat([expected.update(data), expected.final()])); + expect(cipher.getAuthTag()).toEqual(expected.getAuthTag()); + const decipher = crypto.createDecipheriv("aes-128-gcm", key, iv).setAAD(aad).setAuthTag(wrap(cipher.getAuthTag())); + expect(Buffer.concat([decipher.update(encrypted), decipher.final()])).toEqual(data); + + expect(() => crypto.createCipheriv("aes-128-gcm", key, iv).update(wrap(data))).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining( + 'The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.', + ), + }), + ); + }); + + it("as a signature and as a passphrase", () => { + const { privateKey, publicKey } = crypto.generateKeyPairSync("ed25519"); + const data = Buffer.from("data"); + expect(crypto.verify(null, data, publicKey, wrap(crypto.sign(null, data, privateKey)))).toBe(true); + + const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + const signature = crypto.sign("sha256", data, ec.privateKey); + expect(crypto.createVerify("sha256").update(data).verify(ec.publicKey, wrap(signature))).toBe(true); + + const passphrase = wrap(Buffer.from("secret")); + const pem = privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase }); + expect(crypto.createPrivateKey({ key: pem, passphrase }).equals(privateKey)).toBe(true); + expect(crypto.createPrivateKey({ key: pem, passphrase: "secret" }).equals(privateKey)).toBe(true); + }); +}); + +describe("encodings of DiffieHellman and ECDH", () => { + const dh = crypto.createDiffieHellman(64); + dh.generateKeys(); + const ecdh = crypto.createECDH("prime256v1"); + ecdh.generateKeys(); + + it.each([ + ["dh.generateKeys", () => dh.generateKeys("nope")], + ["dh.getPrime", () => dh.getPrime("nope")], + ["dh.getGenerator", () => dh.getGenerator("nope")], + ["dh.getPublicKey", () => dh.getPublicKey("nope")], + ["dh.getPrivateKey", () => dh.getPrivateKey("nope")], + ["dh.computeSecret", () => dh.computeSecret(dh.getPublicKey(), null, "nope")], + ["ecdh.generateKeys", () => ecdh.generateKeys("nope")], + ["ecdh.getPublicKey", () => ecdh.getPublicKey("nope")], + ["ecdh.getPrivateKey", () => ecdh.getPrivateKey("nope")], + ["ecdh.computeSecret", () => ecdh.computeSecret(ecdh.getPublicKey(), null, "nope")], + ["ECDH.convertKey", () => crypto.ECDH.convertKey(ecdh.getPublicKey(), "prime256v1", undefined, "nope")], + ])("%s refuses an output encoding it does not know", (_, call) => { + expect(call).toThrow(expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING", message: "Unknown encoding: nope" })); + }); + + it("anything falsy, and 'buffer', ask for a Buffer", () => { + for (const encoding of [undefined, null, "", 0, false, "buffer"]) { + expect(dh.getGenerator(encoding)).toEqual(Buffer.from([2])); + } + expect(dh.getGenerator("HEX")).toBe("02"); + expect(() => dh.getGenerator(5)).toThrow(expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING" })); + }); + + it("a hex string of odd length loses its last digit, like Buffer.from()", () => { + const other = crypto.createDiffieHellman(dh.getPrime()); + other.setPublicKey("abc", "hex"); + other.setPrivateKey("123", "hex"); + expect([other.getPublicKey("hex"), other.getPrivateKey("hex")]).toEqual(["ab", "12"]); + expect(dh.computeSecret("abc", "hex")).toEqual(dh.computeSecret("ab", "hex")); + + // update() checks it first. + expect(() => crypto.createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)).update("abc", "hex")).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }), + ); + }); +}); + +it("dh.computeSecret() says whether the other key is too small or too large", () => { + const dh = crypto.createDiffieHellman(64); + dh.generateKeys(); + const p = BigInt("0x" + dh.getPrime("hex")); + const outcome = n => { + try { + return dh.computeSecret(n.toString(16).padStart(16, "0"), "hex").length; + } catch (e) { + return `${e.name} ${e.code}: ${e.message}`; + } + }; + const tooSmall = "RangeError ERR_CRYPTO_INVALID_KEYLEN: Supplied key is too small"; + const tooLarge = "RangeError ERR_CRYPTO_INVALID_KEYLEN: Supplied key is too large"; + expect([0n, 1n, 2n, p - 2n, p - 1n, p].map(outcome)).toEqual([tooSmall, tooSmall, 8, 8, tooLarge, tooLarge]); +}); + +it.each(["7", ""])("checkPrime(%j) does not take a string", candidate => { + const error = expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining( + 'The "candidate" argument must be of type bigint or an instance of ArrayBuffer, TypedArray, Buffer, or DataView.', + ), + }); + expect(() => crypto.checkPrimeSync(candidate)).toThrow(error); + expect(() => crypto.checkPrime(candidate, () => {})).toThrow(error); +}); + +describe("Sign and Verify", () => { + const data = Buffer.from("data"); + const rsa = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 }); + const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + const ed = crypto.generateKeyPairSync("ed25519"); + const rsaSignature = crypto.sign("sha256", data, rsa.privateKey); + const verifier = () => crypto.createVerify("sha256").update(data); + + it("can be used once", () => { + const notInitialised = expect.objectContaining({ code: "ERR_CRYPTO_INVALID_STATE", message: "Not initialised" }); + const sign = crypto.createSign("sha256").update(data); + sign.sign(rsa.privateKey); + expect(() => sign.sign(rsa.privateKey)).toThrow(notInitialised); + expect(() => sign.update(data)).toThrow(notInitialised); + const verify = verifier(); + expect(verify.verify(rsa.publicKey, rsaSignature)).toBe(true); + expect(() => verify.verify(rsa.publicKey, rsaSignature)).toThrow(notInitialised); + expect(() => verify.update(data)).toThrow(notInitialised); + }); + + it("do not take a key that signs the message itself", () => { + const unsupported = expect.objectContaining({ + code: "ERR_CRYPTO_UNSUPPORTED_OPERATION", + message: "Unsupported crypto operation", + }); + expect(() => crypto.createSign("sha256").update(data).sign(ed.privateKey)).toThrow(unsupported); + expect(() => verifier().verify(ed.publicKey, Buffer.alloc(64))).toThrow(unsupported); + }); + + it("report what is wrong in Node's order", () => { + const invalidValue = expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }); + const unsupported = expect.objectContaining({ code: "ERR_CRYPTO_UNSUPPORTED_OPERATION" }); + // The options come before the kind of key. + for (const options of [{ padding: "x" }, { saltLength: "x" }, { dsaEncoding: "x" }]) { + expect(() => verifier().verify({ key: ed.publicKey, ...options }, Buffer.alloc(64))).toThrow(invalidValue); + expect(() => + crypto + .createSign("sha256") + .update(data) + .sign({ key: ed.privateKey, ...options }), + ).toThrow(invalidValue); + } + // The kind of key comes before whether the object was used. + const sign = crypto.createSign("sha256").update(data); + sign.sign(rsa.privateKey); + expect(() => sign.sign(ed.privateKey)).toThrow(unsupported); + const verify = verifier(); + verify.verify(rsa.publicKey, rsaSignature); + expect(() => verify.verify(ed.publicKey, Buffer.alloc(64))).toThrow(unsupported); + }); + + it("sign() refuses an output encoding it does not know", () => { + expect(() => crypto.createSign("sha256").update(data).sign(rsa.privateKey, "nope")).toThrow( + expect.objectContaining({ code: "ERR_UNKNOWN_ENCODING", message: "Unknown encoding: nope" }), + ); + expect(crypto.createSign("sha256").update(data).sign(rsa.privateKey, "hex")).toBe(rsaSignature.toString("hex")); + }); + + it("sign() reports why BoringSSL did not sign", () => { + const options = { key: rsa.privateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 9999 }; + expect(() => crypto.createSign("sha256").update(data).sign(options)).toThrow( + expect.objectContaining({ name: "Error", code: "ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE" }), + ); + }); + + it.each([ + ["an unknown padding", { padding: 99 }], + ["no padding", { padding: crypto.constants.RSA_NO_PADDING }], + ["a negative salt length", { padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: -5 }], + ])("verify() is false with %s", (_, options) => { + expect(verifier().verify({ key: rsa.publicKey, ...options }, rsaSignature)).toBe(false); + }); + + it("verify() throws for an ieee-p1363 signature of the wrong length", () => { + const key = { key: ec.publicKey, dsaEncoding: "ieee-p1363" }; + const signature = crypto.sign("sha256", data, { key: ec.privateKey, dsaEncoding: "ieee-p1363" }); + expect(verifier().verify(key, signature)).toBe(true); + for (const malformed of [signature.subarray(1), Buffer.alloc(0), Buffer.concat([signature, Buffer.alloc(1)])]) { + expect(() => verifier().verify(key, malformed)).toThrow( + expect.objectContaining({ code: "ERR_CRYPTO_OPERATION_FAILED", message: "Malformed signature" }), + ); + } + }); + + it("read dsaEncoding from any object", () => { + class Options { + dsaEncoding = "ieee-p1363"; + constructor(key) { + this.key = key; + } + } + for (const wrap of [key => new Proxy({ key, dsaEncoding: "ieee-p1363" }, {}), key => new Options(key)]) { + const signature = crypto.sign("sha256", data, wrap(ec.privateKey)); + expect(signature).toHaveLength(64); + expect(crypto.createSign("sha256").update(data).sign(wrap(ec.privateKey))).toHaveLength(64); + expect(crypto.verify("sha256", data, wrap(ec.publicKey), signature)).toBe(true); + expect(verifier().verify(wrap(ec.publicKey), signature)).toBe(true); + } + }); +}); + +describe("an input encoding", () => { + const cipher = () => crypto.createCipheriv("aes-128-ctr", Buffer.alloc(16), Buffer.alloc(16)); + + it.each(["nope", 5, "buffer", "", undefined])("of %j is UTF-8 in cipher.update()", encoding => { + expect(cipher().update("h\u00e9", encoding)).toEqual(cipher().update(Buffer.from("h\u00e9"))); + }); + + it.each([5, {}, true])("of %j, which is not a string, is UTF-8 where Buffer.from() reads the string", encoding => { + const dh = crypto.getDiffieHellman("modp14"); + dh.generateKeys(); + expect(dh.computeSecret("abcd", encoding)).toEqual(dh.computeSecret(Buffer.from("abcd"))); + const { publicKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + expect(crypto.createVerify("sha256").update("x").verify(publicKey, "signature", encoding)).toBe(false); + }); + + it.each(["BUFFER", "Buffer", "nope"])("of %j is not known where Buffer.from() reads the string", encoding => { + const unknown = expect.objectContaining({ name: "TypeError", code: "ERR_UNKNOWN_ENCODING" }); + const dh = crypto.getDiffieHellman("modp14"); + dh.generateKeys(); + expect(() => dh.computeSecret("abcd", encoding)).toThrow(unknown); + expect(() => crypto.createECDH("prime256v1").setPrivateKey("abcd", encoding)).toThrow(unknown); + const { publicKey } = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + expect(() => crypto.createVerify("sha256").update("x").verify(publicKey, "signature", encoding)).toThrow(unknown); + const iv = Buffer.alloc(12); + expect(() => crypto.createCipheriv("aes-128-gcm", Buffer.alloc(16), iv).setAAD("abcd", { encoding })).toThrow( + unknown, + ); + expect(() => crypto.createDecipheriv("aes-128-gcm", Buffer.alloc(16), iv).setAuthTag("abcd", encoding)).toThrow( + unknown, + ); + expect(dh.computeSecret("abcd", "buffer")).toEqual(dh.computeSecret(Buffer.from("abcd"))); + }); +}); + +describe("crypto.diffieHellman()", () => { + const alice = crypto.generateKeyPairSync("x25519"); + const bob = crypto.generateKeyPairSync("x25519"); + const secret = crypto.diffieHellman({ privateKey: alice.privateKey, publicKey: bob.publicKey }); + + it.each([ + ["PEM", { type: "pkcs8", format: "pem" }, { type: "spki", format: "pem" }], + ["DER", { type: "pkcs8", format: "der" }, { type: "spki", format: "der" }], + ["JWK", { format: "jwk" }, { format: "jwk" }], + ])("takes keys as %s", async (_, privateEncoding, publicEncoding) => { + const options = { + privateKey: { key: alice.privateKey.export(privateEncoding), ...privateEncoding }, + publicKey: { key: bob.publicKey.export(publicEncoding), ...publicEncoding }, + }; + expect(crypto.diffieHellman(options)).toEqual(secret); + expect(crypto.diffieHellman({ ...options, publicKey: bob.publicKey })).toEqual(secret); + expect(crypto.diffieHellman({ ...options, privateKey: alice.privateKey })).toEqual(secret); + expect(await util.promisify(crypto.diffieHellman)(options)).toEqual(secret); + }); + + it("takes a PEM string, and a passphrase", () => { + const pem = alice.privateKey.export({ type: "pkcs8", format: "pem" }); + expect(crypto.diffieHellman({ privateKey: pem, publicKey: bob.publicKey })).toEqual(secret); + const encrypted = alice.privateKey.export({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase: "x" }); + const privateKey = { key: encrypted, passphrase: "x" }; + expect(crypto.diffieHellman({ privateKey, publicKey: bob.publicKey })).toEqual(secret); + }); + + it("names the option and the kinds of key in its errors", () => { + const ec = crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }); + const ed = crypto.generateKeyPairSync("ed25519"); + expect(() => crypto.diffieHellman({ privateKey: alice.privateKey, publicKey: ec.publicKey })).toThrow( + expect.objectContaining({ + code: "ERR_CRYPTO_INCOMPATIBLE_KEY", + message: "Incompatible key types for Diffie-Hellman: x25519 and ec", + }), + ); + expect(() => crypto.diffieHellman(ed)).toThrow( + expect.objectContaining({ + code: "ERR_CRYPTO_INCOMPATIBLE_KEY", + message: "Incompatible key types for Diffie-Hellman: ed25519 and ed25519", + }), + ); + expect(() => crypto.diffieHellman({ privateKey: alice.privateKey, publicKey: 5 })).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining('The "options.publicKey" property must be of type string or an instance of'), + }), + ); + expect(() => crypto.diffieHellman({ privateKey: { key: 5 }, publicKey: bob.publicKey })).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + message: expect.stringContaining( + 'The "options.privateKey.key" property must be of type string or an instance of', + ), + }), + ); + expect(() => crypto.diffieHellman({ privateKey: null, publicKey: bob.publicKey })).toThrow( + expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" }), + ); + }); + + it("names the option in an error about a JWK or a raw key", () => { + const raw = bob.publicKey.export({ format: "raw-public" }); + const withPublicKey = publicKey => () => crypto.diffieHellman({ privateKey: alice.privateKey, publicKey }); + const withPrivateKey = privateKey => () => crypto.diffieHellman({ privateKey, publicKey: bob.publicKey }); + const error = (code, message) => expect.objectContaining({ name: "TypeError", code, message }); + + for (const [key, received] of [ + [5, "Received type number (5)"], + [[], "Received an instance of Array"], + [null, "Received null"], + ]) { + expect(withPublicKey({ key, format: "jwk" })).toThrow( + error("ERR_INVALID_ARG_TYPE", `The "options.publicKey.key" property must be of type object. ${received}`), + ); + } + expect(withPrivateKey({ key: "x", format: "jwk" })).toThrow( + error( + "ERR_INVALID_ARG_TYPE", + `The "options.privateKey.key" property must be of type object. Received type string ('x')`, + ), + ); + expect(withPrivateKey({ key: raw, format: "raw-public", asymmetricKeyType: "x25519" })).toThrow( + error("ERR_INVALID_ARG_VALUE", "The property 'options.privateKey.format' is invalid. Received 'raw-public'"), + ); + expect(withPublicKey({ key: raw, format: "raw-public" })).toThrow( + error( + "ERR_INVALID_ARG_TYPE", + 'The "options.publicKey.asymmetricKeyType" property must be of type string. Received undefined', + ), + ); + expect(withPublicKey({ key: raw, format: "raw-public", asymmetricKeyType: "ec" })).toThrow( + error( + "ERR_INVALID_ARG_TYPE", + 'The "options.publicKey.namedCurve" property must be of type string. Received undefined', + ), + ); + expect(withPublicKey({ key: raw, format: "raw-public", asymmetricKeyType: "x25519" })()).toEqual(secret); + + // Elsewhere the key is the argument `key`. + expect(() => crypto.createPublicKey({ key: 5, format: "jwk" })).toThrow( + error("ERR_INVALID_ARG_TYPE", 'The "key.key" property must be of type object. Received type number (5)'), + ); + }); +}); diff --git a/test/js/node/crypto/sign-jwk-ieee-p1363.test.ts b/test/js/node/crypto/sign-jwk-ieee-p1363.test.ts index 2b94737128b5..518a0f89fa3e 100644 --- a/test/js/node/crypto/sign-jwk-ieee-p1363.test.ts +++ b/test/js/node/crypto/sign-jwk-ieee-p1363.test.ts @@ -140,10 +140,15 @@ test("crypto.Verify with ieee-p1363 rejects signatures that are not in P1363 for // When the caller requests ieee-p1363, a signature that is not 2*n bytes of // raw r||s must fail verification rather than being reinterpreted as DER. + // Like Node, Verify throws for one and crypto.verify() returns false. { + const key = { key: publicJwk, format: "jwk", dsaEncoding: "ieee-p1363" } as const; const verifier = crypto.createVerify("sha256"); verifier.update(testData); - expect(verifier.verify({ key: publicJwk, format: "jwk", dsaEncoding: "ieee-p1363" }, derSignature)).toBe(false); + expect(() => verifier.verify(key, derSignature)).toThrow( + expect.objectContaining({ code: "ERR_CRYPTO_OPERATION_FAILED", message: "Malformed signature" }), + ); + expect(crypto.verify("sha256", Buffer.from(testData), key, derSignature)).toBe(false); } // The legitimate case still works: a real 64-byte P1363 signature verifies. diff --git a/test/js/node/crypto/x509.test.ts b/test/js/node/crypto/x509.test.ts index a704de27e7d8..f7ce9071d2bd 100644 --- a/test/js/node/crypto/x509.test.ts +++ b/test/js/node/crypto/x509.test.ts @@ -56,6 +56,34 @@ qGl/+1Di3abpA8YZOtoyAiAiScaiKhxu48bWQXYW5ZoQNzAfBIwL4krTuLVAKYZc Vg== -----END CERTIFICATE-----`; +describe("new X509Certificate()", () => { + test.each([ + ["nothing", [], "Received undefined"], + ["null", [null], "Received null"], + ["a number", [5], "Received type number (5)"], + ["an object", [{}], "Received an instance of Object"], + ])("with %s", (_, args, received) => { + expect(() => new (X509Certificate as any)(...args)).toThrow( + expect.objectContaining({ + name: "TypeError", + code: "ERR_INVALID_ARG_TYPE", + message: `The "buffer" argument must be of type string or an instance of Buffer, TypedArray, or DataView. ${received}`, + }), + ); + }); + + test("with a PEM that starts with a byte order mark", () => { + const pem = readFileSync(path.join(import.meta.dir, "..", "test", "fixtures", "keys", "agent1-cert.pem"), "utf8"); + expect(new X509Certificate("\ufeff" + pem).fingerprint256).toBe(new X509Certificate(pem).fingerprint256); + }); + + test("with a detached view, which is empty", () => { + const view = new Uint8Array(4); + view.buffer.transfer(); + expect(() => new X509Certificate(view)).toThrow(expect.objectContaining({ name: "Error" })); + }); +}); + describe("X509Certificate.checkHost()", () => { const cert = new X509Certificate(wildcardSanCertPem); const cnOnly = new X509Certificate(cnOnlyCertPem); diff --git a/test/js/node/test/parallel/test-crypto-dh.js b/test/js/node/test/parallel/test-crypto-dh.js index 8a3dee5b0756..7b9a9d50d890 100644 --- a/test/js/node/test/parallel/test-crypto-dh.js +++ b/test/js/node/test/parallel/test-crypto-dh.js @@ -7,11 +7,14 @@ if (!common.hasCrypto) { const assert = require('assert'); const crypto = require('crypto'); const { - hasOpenSSL3, + hasOpenSSL, + hasFIPS, + isBoringSSL, } = require('../common/crypto'); { - const size = crypto.getFips() || hasOpenSSL3 ? 1024 : 256; + const size = hasFIPS(3) ? + 2048 : (crypto.getFips() === 1 || hasOpenSSL(3) ? 1024 : 256); const dh1 = crypto.createDiffieHellman(size); const p1 = dh1.getPrime('buffer'); const dh2 = crypto.createDiffieHellman(p1, 'buffer'); @@ -57,7 +60,7 @@ const { assert.strictEqual(secret1, secret4); let wrongBlockLength; - if (hasOpenSSL3) { + if (hasOpenSSL(3)) { wrongBlockLength = { message: /wrong[\s_]final[\s_]block[\s_]length/i, code: /ERR_OSSL_(EVP_)?WRONG_FINAL_BLOCK_LENGTH/, @@ -91,16 +94,14 @@ const { { assert.throws(() => { dh3.computeSecret(''); - }, { message: process.features.openssl_is_boringssl ? - 'Supplied key is invalid' : - 'Supplied key is too small' }); + }, { message: 'Supplied key is too small' }); } } // Through a fluke of history, g=0 defaults to DH_GENERATOR (2). { const g = 0; - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.throws(() => crypto.createDiffieHellman('abcdef', g), { code: 'ERR_CRYPTO_OPERATION_FAILED', name: 'Error' @@ -112,7 +113,7 @@ const { } { - if (process.features.openssl_is_boringssl) { + if (isBoringSSL) { assert.throws(() => crypto.createDiffieHellman('abcdef', Buffer.from([2])), { code: 'ERR_CRYPTO_OPERATION_FAILED', name: 'Error' diff --git a/test/js/node/test/parallel/test-crypto-sign-verify.js b/test/js/node/test/parallel/test-crypto-sign-verify.js index 17f07f59d25a..32410d671f43 100644 --- a/test/js/node/test/parallel/test-crypto-sign-verify.js +++ b/test/js/node/test/parallel/test-crypto-sign-verify.js @@ -495,8 +495,8 @@ assert.throws( assert.throws(() => crypto.sign(null, data, input), errObj); assert.throws(() => crypto.verify(null, data, input, sig), errObj); - errObj.message = 'The "signature" argument must be an instance of ' + - 'Buffer, TypedArray, or DataView.' + + errObj.message = 'The "signature" argument must be of type string or an instance of ' + + 'ArrayBuffer, Buffer, TypedArray, or DataView.' + common.invalidArgTypeHelper(input); assert.throws(() => crypto.verify(null, data, 'test', input), errObj); }); @@ -766,13 +766,13 @@ assert.throws( for (const { publicKey, privateKey } of keys) { assert.throws(() => { crypto.createSign('SHA256').update('Test123').sign(privateKey); - }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + }, { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: 'Unsupported crypto operation' }); assert.throws(() => { crypto.createVerify('SHA256').update('Test123').verify(privateKey, 'sig'); - }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + }, { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: 'Unsupported crypto operation' }); assert.throws(() => { crypto.createVerify('SHA256').update('Test123').verify(publicKey, 'sig'); - }, { code: common.openSSLIsBoringSSL ? 'ERR_OSSL_EVP_COMMAND_NOT_SUPPORTED' : 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: common.openSSLIsBoringSSL ? /public key.*COMMAND_NOT_SUPPORTED/ : 'Unsupported crypto operation' }); + }, { code: 'ERR_CRYPTO_UNSUPPORTED_OPERATION', message: 'Unsupported crypto operation' }); } } diff --git a/test/js/web/crypto/web-crypto.test.ts b/test/js/web/crypto/web-crypto.test.ts index c469528f324f..98de89a66fe2 100644 --- a/test/js/web/crypto/web-crypto.test.ts +++ b/test/js/web/crypto/web-crypto.test.ts @@ -160,12 +160,13 @@ describe("Web Crypto", () => { }); // Previously this promise never settled: the error thrown during - // JsonWebKey dictionary conversion (here an invalid key_ops enum entry) + // JsonWebKey dictionary conversion (here a key_ops that is not a sequence) // escaped as an uncaught exception and the DeferredPromise was left in - // m_pendingPromises forever. kty is optional now, so an invalid enum is - // what still reaches the conversion-exception branch. + // m_pendingPromises forever. kty is optional now, and a key_ops entry that + // is not known is skipped, so this is what still reaches the + // conversion-exception branch. it("settles when JsonWebKey dictionary conversion itself throws", async () => { - const { key, iv, wrapped } = await setup(new TextEncoder().encode(JSON.stringify({ key_ops: ["bogus"] }))); + const { key, iv, wrapped } = await setup(new TextEncoder().encode(JSON.stringify({ key_ops: 5 }))); const err = await crypto.subtle .unwrapKey("jwk", wrapped, key, { name: "AES-GCM", iv }, { name: "AES-GCM" }, true, ["encrypt", "decrypt"]) .then( @@ -173,7 +174,7 @@ describe("Web Crypto", () => { e => e, ); expect(err).toBeInstanceOf(TypeError); - expect(err.message).toBe("value must be enumeration (string)"); + expect(err.message).toBe("Value is not a sequence"); }); it("does not leak DeferredPromise in m_pendingPromises on JWK parse errors", async () => { @@ -189,13 +190,13 @@ describe("Web Crypto", () => { const key = await crypto.subtle.importKey("raw", keyData, { name: "AES-GCM" }, false, [ "encrypt", "decrypt", "wrapKey", "unwrapKey", ]); - // key_ops with an invalid enum entry throws during dictionary + // A key_ops that is not a sequence throws during dictionary // conversion, which is the leaky branch this fixture guards (a // merely-unusable JWK now converts fine and rejects downstream). const wrapped = await crypto.subtle.encrypt( { name: "AES-GCM", iv }, key, - new TextEncoder().encode(JSON.stringify({ key_ops: ["bogus"] })), + new TextEncoder().encode(JSON.stringify({ key_ops: 5 })), ); async function once() { await crypto.subtle @@ -444,7 +445,7 @@ describe("RSA-PSS saltLength", () => { // negative values select a salt length: -1 is the digest length and -2 means // "whatever fits" on sign and "accept any salt length" on verify. 2**32 - 1 // and 2**32 - 2 used to turn into exactly those two values. - it("rejects values that do not fit in an int instead of treating them as the -1/-2 selectors", async () => { + it("does not treat values that do not fit in an int as the -1/-2 selectors", async () => { const { privateKey, publicKey } = await crypto.subtle.generateKey( { name: "RSA-PSS", modulusLength: 1024, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, false, @@ -492,10 +493,10 @@ describe("RSA-PSS saltLength", () => { "94": "signed", }, verify: { - "2**32 - 1": "rejected OperationError", - "2**32 - 2": "rejected OperationError", - "2**31": "rejected OperationError", // A salt length that does not match the signature is a failed verification, not an error. + "2**32 - 1": "false", + "2**32 - 2": "false", + "2**31": "false", "32": "false", "20": "true", }, @@ -1749,6 +1750,17 @@ describe("JWK import validation", () => { } }); + it("takes a key_ops with many entries", async () => { + const jwk = { kty: "oct", k: Buffer.alloc(16).toString("base64url") }; + const many = Array.from({ length: 10_000 }, (_, i) => `operation ${i}`); + const importWith = (key_ops: string[]) => + crypto.subtle.importKey("jwk", { ...jwk, key_ops }, "AES-GCM", true, ["encrypt"]); + expect({ + distinct: await rejection(importWith([...many, "encrypt"])), + lastRepeatsFirst: await rejection(importWith(["encrypt", ...many, many[0]])), + }).toEqual({ distinct: "resolved", lastRepeatsFirst: "DataError" }); + }); + it("reports the modulus length in bits", async () => { const n = Buffer.alloc(256); n[0] = 0x40; @@ -1785,3 +1797,115 @@ describe("AES-CTR", () => { ); }); }); + +describe.each([ + ["Ed25519", "70", ["verify"], ["sign"]], + ["X25519", "6e", [], ["deriveBits"]], +] as const)("%s keys in DER", (name, oid, publicUsages, privateUsages) => { + const key = Buffer.alloc(32, 0x11).toString("hex"); + const bytes = (hex: string) => Buffer.from(hex.replaceAll(" ", ""), "hex"); + const dataError = expect.objectContaining({ name: "DataError" }); + + it("are imported", async () => { + const spki = bytes(`302a 3005 0603 2b65${oid} 0321 00 ${key}`); + const publicKey = await crypto.subtle.importKey("spki", spki, name, true, publicUsages); + expect(Buffer.from(await crypto.subtle.exportKey("raw", publicKey)).toString("hex")).toBe(key); + expect(Buffer.from(await crypto.subtle.exportKey("spki", publicKey))).toEqual(spki); + + const pkcs8 = bytes(`302e 020100 3005 0603 2b65${oid} 0422 0420 ${key}`); + const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, name, true, privateUsages); + expect(Buffer.from(await crypto.subtle.exportKey("pkcs8", privateKey))).toEqual(pkcs8); + + // The attributes of a PrivateKeyInfo are optional, and are not kept. + const withAttributes = bytes(`3030 020100 3005 0603 2b65${oid} 0422 0420 ${key} a000`); + const sameKey = await crypto.subtle.importKey("pkcs8", withAttributes, name, true, privateUsages); + expect(Buffer.from(await crypto.subtle.exportKey("pkcs8", sameKey))).toEqual(pkcs8); + }); + + it.each([ + ["another outer tag", `312a 3005 0603 2b65${oid} 0321 00 ${key}`], + ["no outer tag", `002a 3005 0603 2b65${oid} 0321 00 ${key}`], + ["an outer length that is too long", `302b 3005 0603 2b65${oid} 0321 00 ${key}`], + ["an outer length that is too short", `3029 3005 0603 2b65${oid} 0321 00 ${key}`], + ["an outer length of 0", `3000 3005 0603 2b65${oid} 0321 00 ${key}`], + ["another tag for the algorithm", `302a 3105 0603 2b65${oid} 0321 00 ${key}`], + ["a wrong length for the algorithm", `302a 3006 0603 2b65${oid} 0321 00 ${key}`], + ["a BIT STRING that is longer than the data", `302a 3005 0603 2b65${oid} 0322 00 ${key}`], + ["parameters", `302c 3007 0603 2b65${oid} 0500 0321 00 ${key}`], + ["unused bits", `302a 3005 0603 2b65${oid} 0321 01 ${key}`], + ["a byte after the end", `302a 3005 0603 2b65${oid} 0321 00 ${key} 00`], + ["a key of 31 bytes", `3029 3005 0603 2b65${oid} 0320 00 ${key.slice(2)}`], + ["nothing", ``], + ])("are refused as spki with %s", async (_, hex) => { + expect(crypto.subtle.importKey("spki", bytes(hex), name, true, publicUsages)).rejects.toThrow(dataError); + }); + + it.each([ + ["another outer tag", `312e 020100 3005 0603 2b65${oid} 0422 0420 ${key}`], + ["an outer length that is too long", `302f 020100 3005 0603 2b65${oid} 0422 0420 ${key}`], + ["an outer length that is too short", `302d 020100 3005 0603 2b65${oid} 0422 0420 ${key}`], + ["version 2", `302e 020102 3005 0603 2b65${oid} 0422 0420 ${key}`], + ["another tag for the version", `302e 040100 3005 0603 2b65${oid} 0422 0420 ${key}`], + ["an inner length that is too short", `302e 020100 3005 0603 2b65${oid} 0422 041f ${key}`], + ["another inner tag", `302e 020100 3005 0603 2b65${oid} 0422 0320 ${key}`], + ["parameters", `3030 020100 3005 0603 2b65${oid} 0500 0422 0420 ${key}`], + ["a byte after the end", `302e 020100 3005 0603 2b65${oid} 0422 0420 ${key} 00`], + ["a key of 33 bytes", `302f 020100 3005 0603 2b65${oid} 0423 0421 ${key}11`], + ["nothing", ``], + ])("are refused as pkcs8 with %s", async (_, hex) => { + expect(crypto.subtle.importKey("pkcs8", bytes(hex), name, true, privateUsages)).rejects.toThrow(dataError); + }); +}); + +describe("key_ops of a JWK", () => { + const jwk = { kty: "oct", k: "AAAAAAAAAAAAAAAAAAAAAA", alg: "A128GCM", ext: true }; + const importKey = (key_ops: unknown) => + crypto.subtle.importKey("jwk", { ...jwk, key_ops } as JsonWebKey, "AES-GCM", true, ["encrypt"]); + + it.each([[["encrypt", "nope"]], [["nope", "encrypt", "decrypt", "other"]], [["encrypt", 5]]])( + "%j: what is not known is skipped", + async key_ops => { + expect((await importKey(key_ops)).usages).toEqual(["encrypt"]); + }, + ); + + it.each([[["nope"]], [["Encrypt"]], [["encrypt", "nope", "nope"]], [["encrypt", "encrypt"]], [[]]])( + "%j is a DataError", + async key_ops => { + expect(importKey(key_ops)).rejects.toThrow(expect.objectContaining({ name: "DataError" })); + }, + ); + + it.each(["encrypt", 5, null])("%j is a TypeError", async key_ops => { + expect(importKey(key_ops)).rejects.toThrow(TypeError); + }); + + it("is skipped the same way when a key is unwrapped", async () => { + const wrappingKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 128 }, false, [ + "encrypt", + "unwrapKey", + ]); + const algorithm = { name: "AES-GCM", iv: new Uint8Array(12) }; + const text = Buffer.from(JSON.stringify({ ...jwk, key_ops: ["encrypt", "nope"] })); + const wrapped = await crypto.subtle.encrypt(algorithm, wrappingKey, text); + const key = await crypto.subtle.unwrapKey("jwk", wrapped, wrappingKey, algorithm, "AES-GCM", true, ["encrypt"]); + expect(key.usages).toEqual(["encrypt"]); + }); +}); + +it.each(["ECDSA", "ECDH"])("%s: a pkcs8 key that came with a compressed point leaves with a whole one", async name => { + const compressed = Buffer.from( + "3067020100301306072a8648ce3d020106082a8648ce3d030107044d304b0201010420c9afa9d845ba75166b5c215767b1d6934e50c3db36e89b127b8a622b120f6721a12403220003" + + "60fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb6", + "hex", + ); + const algorithm = { name, namedCurve: "P-256" }; + const key = await crypto.subtle.importKey("pkcs8", compressed, algorithm, true, [ + name === "ECDSA" ? "sign" : "deriveBits", + ]); + const exported = Buffer.from(await crypto.subtle.exportKey("pkcs8", key)); + expect(exported).toHaveLength(138); + expect(exported.subarray(-65).toString("hex")).toBe( + "0460fed4ba255a9d31c961eb74c6356d68c049b8923b61fa6ce669622e60f29fb67903fe1008b8bc99a41ae9e95628bc64f2f1b20c2d7e9f5177a3c294d4462299", + ); +});