diff --git a/bench/crypto/asymmetricCipher.js b/bench/crypto/asymmetricCipher.js new file mode 100644 index 0000000000..588136235b --- /dev/null +++ b/bench/crypto/asymmetricCipher.js @@ -0,0 +1,24 @@ +import { bench, run } from "mitata"; +const crypto = require("node:crypto"); + +const keyPair = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { + type: "spki", + format: "pem", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "pem", + }, +}); + +// Max message size for 2048-bit RSA keys +const plaintext = crypto.getRandomValues(Buffer.alloc(214)); + +bench("RSA_PKCS1_OAEP_PADDING round-trip", () => { + const ciphertext = crypto.publicEncrypt(keyPair.publicKey, plaintext); + crypto.privateDecrypt(keyPair.privateKey, ciphertext); +}); + +await run(); diff --git a/src/bun.js/bindings/KeyObject.cpp b/src/bun.js/bindings/KeyObject.cpp index 97b1e395a6..901e325602 100644 --- a/src/bun.js/bindings/KeyObject.cpp +++ b/src/bun.js/bindings/KeyObject.cpp @@ -21,6 +21,7 @@ // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. +#include "ErrorCode.h" #include "KeyObject.h" #include "JavaScriptCore/JSArrayBufferView.h" #include "JavaScriptCore/JSCJSValue.h" @@ -47,18 +48,19 @@ #include "JSBuffer.h" #include "CryptoAlgorithmHMAC.h" #include "CryptoAlgorithmEd25519.h" +#include "CryptoAlgorithmRSA_OAEP.h" #include "CryptoAlgorithmRSA_PSS.h" #include "CryptoAlgorithmRSASSA_PKCS1_v1_5.h" #include "CryptoAlgorithmECDSA.h" #include "CryptoAlgorithmEcdsaParams.h" +#include "CryptoAlgorithmRsaOaepParams.h" #include "CryptoAlgorithmRsaPssParams.h" #include "CryptoAlgorithmRegistry.h" #include "wtf/ForbidHeapAllocation.h" #include "wtf/Noncopyable.h" using namespace JSC; using namespace Bun; -using JSGlobalObject - = JSC::JSGlobalObject; +using JSGlobalObject = JSC::JSGlobalObject; using Exception = JSC::Exception; using JSValue = JSC::JSValue; using JSString = JSC::JSString; @@ -81,6 +83,8 @@ JSC_DECLARE_HOST_FUNCTION(KeyObject__generateKeySync); JSC_DECLARE_HOST_FUNCTION(KeyObject__generateKeyPairSync); JSC_DECLARE_HOST_FUNCTION(KeyObject__Sign); JSC_DECLARE_HOST_FUNCTION(KeyObject__Verify); +JSC_DECLARE_HOST_FUNCTION(KeyObject__publicEncrypt); +JSC_DECLARE_HOST_FUNCTION(KeyObject__privateDecrypt); namespace WebCore { @@ -571,7 +575,7 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__createPrivateKey, (JSC::JSGlobalObject * glo return JSValue::encode(JSC::jsUndefined()); } auto pKeyID = EVP_PKEY_id(pkey.get()); - auto impl = CryptoKeyRSA::create(pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5, CryptoAlgorithmIdentifier::SHA_1, false, CryptoKeyType::Private, WTFMove(pkey), true, CryptoKeyUsageDecrypt); + auto impl = CryptoKeyRSA::create(pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSA_OAEP, CryptoAlgorithmIdentifier::SHA_1, false, CryptoKeyType::Private, WTFMove(pkey), true, CryptoKeyUsageDecrypt); return JSC::JSValue::encode(JSCryptoKey::create(structure, zigGlobalObject, WTFMove(impl))); } else if (type == "pkcs8"_s) { @@ -1165,11 +1169,11 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__createPublicKey, (JSC::JSGlobalObject * glob } auto pKeyID = EVP_PKEY_id(pkey.get()); - return KeyObject__createRSAFromPrivate(globalObject, pkey.get(), pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5); + return KeyObject__createRSAFromPrivate(globalObject, pkey.get(), pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSA_OAEP); } auto pKeyID = EVP_PKEY_id(pkey.get()); - auto impl = CryptoKeyRSA::create(pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSAES_PKCS1_v1_5, CryptoAlgorithmIdentifier::SHA_1, false, CryptoKeyType::Public, WTFMove(pkey), true, CryptoKeyUsageEncrypt); + auto impl = CryptoKeyRSA::create(pKeyID == EVP_PKEY_RSA_PSS ? CryptoAlgorithmIdentifier::RSASSA_PKCS1_v1_5 : CryptoAlgorithmIdentifier::RSA_OAEP, CryptoAlgorithmIdentifier::SHA_1, false, CryptoKeyType::Public, WTFMove(pkey), true, CryptoKeyUsageEncrypt); return JSC::JSValue::encode(JSCryptoKey::create(structure, zigGlobalObject, WTFMove(impl))); } else if (type == "spki"_s) { // We use d2i_PUBKEY() to import a public key. @@ -2943,6 +2947,149 @@ JSC_DEFINE_HOST_FUNCTION(KeyObject__SymmetricKeySize, (JSC::JSGlobalObject * glo return JSC::JSValue::encode(JSC::jsUndefined()); } +static EncodedJSValue doAsymmetricCipher(JSGlobalObject* globalObject, CallFrame* callFrame, bool encrypt) +{ + auto count = callFrame->argumentCount(); + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (count != 2) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_MISSING_ARGS, + "expected object as first argument"_s); + } + + auto* jsKey = jsDynamicCast(callFrame->uncheckedArgument(0)); + if (!jsKey) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected object as first argument"_s); + } + + auto jsCryptoKeyValue = jsKey->getIfPropertyExists( + globalObject, PropertyName(Identifier::fromString(vm, "key"_s))); + if (jsCryptoKeyValue.isUndefinedOrNull() || jsCryptoKeyValue.isEmpty()) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected key property in key object"_s); + } + auto* jsCryptoKey = jsDynamicCast(jsCryptoKeyValue); + + auto& cryptoKey = jsCryptoKey->wrapped(); + // We should only encrypt to public keys, and decrypt with private keys. + if ((encrypt && cryptoKey.type() != CryptoKeyType::Public) + || (!encrypt && cryptoKey.type() != CryptoKeyType::Private) + // RSA-OAEP is the modern alternative to RSAES-PKCS1-v1_5, which is vulnerable to + // known-ciphertext attacks. Node.js does not support it either. + || cryptoKey.algorithmIdentifier() != CryptoAlgorithmIdentifier::RSA_OAEP) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_VALUE, + "unsupported key type for asymmetric encryption"_s); + } + + bool setCustomHash = false; + auto oaepHash = WebCore::CryptoAlgorithmIdentifier::SHA_1; + auto jsOaepHash = jsKey->getIfPropertyExists( + globalObject, PropertyName(Identifier::fromString(vm, "oaepHash"_s))); + if (!jsOaepHash.isUndefined() && !jsOaepHash.isEmpty()) { + if (UNLIKELY(!jsOaepHash.isString())) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected string for oaepHash"_s); + } + auto oaepHashStr = jsOaepHash.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, encodedJSValue()); + + auto oaepHashId = CryptoAlgorithmRegistry::singleton().identifier(oaepHashStr); + if (UNLIKELY(!oaepHashId)) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_CRYPTO_INVALID_DIGEST, + "unsupported digest for oaepHash"_s); + } + switch (*oaepHashId) { + case WebCore::CryptoAlgorithmIdentifier::SHA_1: + case WebCore::CryptoAlgorithmIdentifier::SHA_224: + case WebCore::CryptoAlgorithmIdentifier::SHA_256: + case WebCore::CryptoAlgorithmIdentifier::SHA_384: + case WebCore::CryptoAlgorithmIdentifier::SHA_512: { + setCustomHash = true; + oaepHash = *oaepHashId; + break; + } + default: { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_CRYPTO_INVALID_DIGEST, + "unsupported digest for oaepHash"_s); + } + } + } + + std::optional oaepLabel = std::nullopt; + auto jsOaepLabel = jsKey->getIfPropertyExists( + globalObject, PropertyName(Identifier::fromString(vm, "oaepLabel"_s))); + if (!jsOaepLabel.isUndefined() && !jsOaepLabel.isEmpty()) { + if (UNLIKELY(!jsOaepLabel.isCell())) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected Buffer or array-like object for oaepLabel"_s); + } + auto jsOaepLabelCell = jsOaepLabel.asCell(); + auto jsOaepLabelType = jsOaepLabelCell->type(); + + if (isTypedArrayTypeIncludingDataView(jsOaepLabelType)) { + auto* jsBufferView = jsCast(jsOaepLabelCell); + oaepLabel = std::optional{jsBufferView->unsharedImpl()}; + } else if (jsOaepLabelType == ArrayBufferType) { + auto* jsBuffer = jsDynamicCast(jsOaepLabelCell); + oaepLabel = std::optional{jsBuffer->impl()}; + } else { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected Buffer or array-like object for oaepLabel"_s); + } + } + + auto padding = RSA_PKCS1_OAEP_PADDING; + auto jsPadding = jsKey->getIfPropertyExists( + globalObject, PropertyName(Identifier::fromString(vm, "padding"_s))); + if (!jsPadding.isUndefinedOrNull() && !jsPadding.isEmpty()) { + if (UNLIKELY(!jsPadding.isNumber())) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected number for padding"_s); + } + padding = jsPadding.toUInt32(globalObject); + if (padding == RSA_PKCS1_PADDING && !encrypt) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_VALUE, + "RSA_PKCS1_PADDING is no longer supported for private decryption"_s); + } + if (padding != RSA_PKCS1_OAEP_PADDING && (setCustomHash || oaepLabel.has_value())) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_VALUE, + "oaepHash/oaepLabel cannot be set without RSA_PKCS1_OAEP_PADDING"_s); + } + } + + auto jsBuffer = KeyObject__GetBuffer(callFrame->uncheckedArgument(1)); + if (jsBuffer.hasException()) { + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_ARG_TYPE, + "expected Buffer or array-like object as second argument"_s); + } + auto buffer = jsBuffer.releaseReturnValue(); + + auto params = CryptoAlgorithmRsaOaepParams{}; + params.label = oaepLabel; + params.padding = padding; + const auto& rsaKey = downcast(cryptoKey); + auto operation = encrypt ? CryptoAlgorithmRSA_OAEP::platformEncryptWithHash : CryptoAlgorithmRSA_OAEP::platformDecryptWithHash; + auto result = operation(params, rsaKey, buffer, oaepHash); + if (result.hasException()) { + WebCore::propagateException(*globalObject, scope, result.releaseException()); + return encodedJSUndefined(); + } + auto outBuffer = result.releaseReturnValue(); + return JSValue::encode(WebCore::createBuffer(globalObject, outBuffer)); +} + +JSC_DEFINE_HOST_FUNCTION(KeyObject__publicEncrypt, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + return doAsymmetricCipher(globalObject, callFrame, true); +} + +JSC_DEFINE_HOST_FUNCTION(KeyObject__privateDecrypt, (JSGlobalObject* globalObject, CallFrame* callFrame)) +{ + return doAsymmetricCipher(globalObject, callFrame, false); +} + JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) { VM& vm = globalObject->vm(); @@ -2974,6 +3121,11 @@ JSValue createNodeCryptoBinding(Zig::GlobalObject* globalObject) obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "sign"_s)), JSC::JSFunction::create(vm, globalObject, 3, "sign"_s, KeyObject__Sign, ImplementationVisibility::Public, NoIntrinsic), 0); obj->putDirect(vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "verify"_s)), JSC::JSFunction::create(vm, globalObject, 4, "verify"_s, KeyObject__Verify, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "publicEncrypt"_s)), + JSFunction::create(vm, globalObject, 2, "publicEncrypt"_s, KeyObject__publicEncrypt, ImplementationVisibility::Public, NoIntrinsic), 0); + obj->putDirect(vm, PropertyName(Identifier::fromString(vm, "privateDecrypt"_s)), + JSFunction::create(vm, globalObject, 2, "privateDecrypt"_s, KeyObject__privateDecrypt, ImplementationVisibility::Public, NoIntrinsic), 0); + return obj; } diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEP.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEP.h index dba7ea9d5d..582e3e4404 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEP.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEP.h @@ -40,6 +40,9 @@ public: static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::RSA_OAEP; static Ref create(); + static ExceptionOr> platformEncryptWithHash(const CryptoAlgorithmRsaOaepParams&, const CryptoKeyRSA&, const Vector&, CryptoAlgorithmIdentifier hashIdentifier); + static ExceptionOr> platformDecryptWithHash(const CryptoAlgorithmRsaOaepParams&, const CryptoKeyRSA&, const Vector&, CryptoAlgorithmIdentifier hashIdentifier); + private: CryptoAlgorithmRSA_OAEP() = default; CryptoAlgorithmIdentifier identifier() const final; diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEPOpenSSL.cpp b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEPOpenSSL.cpp index 3095000a61..681aaf9ec1 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEPOpenSSL.cpp +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRSA_OAEPOpenSSL.cpp @@ -38,11 +38,11 @@ namespace WebCore { ExceptionOr> CryptoAlgorithmRSA_OAEP::platformEncrypt(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector& plainText) { -#if 1 // defined(EVP_PKEY_CTX_set_rsa_oaep_md) && defined(EVP_PKEY_CTX_set_rsa_mgf1_md) && defined(EVP_PKEY_CTX_set0_rsa_oaep_label) - const EVP_MD* md = digestAlgorithm(key.hashAlgorithmIdentifier()); - if (!md) - return Exception { NotSupportedError }; + return CryptoAlgorithmRSA_OAEP::platformEncryptWithHash(parameters, key, plainText, key.hashAlgorithmIdentifier()); +} +ExceptionOr> CryptoAlgorithmRSA_OAEP::platformEncryptWithHash(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector& plainText, CryptoAlgorithmIdentifier hashIdentifier) +{ auto ctx = EvpPKeyCtxPtr(EVP_PKEY_CTX_new(key.platformKey(), nullptr)); if (!ctx) return Exception { OperationError }; @@ -50,14 +50,25 @@ ExceptionOr> CryptoAlgorithmRSA_OAEP::platformEncrypt(const Cryp if (EVP_PKEY_encrypt_init(ctx.get()) <= 0) return Exception { OperationError }; - if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) + auto padding = parameters.padding; + if (padding == 0) { + padding = RSA_PKCS1_OAEP_PADDING; + } + + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) <= 0) return Exception { OperationError }; - if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), md) <= 0) - return Exception { OperationError }; + if (padding == RSA_PKCS1_OAEP_PADDING) { + const EVP_MD* md = digestAlgorithm(hashIdentifier); + if (!md) + return Exception { NotSupportedError }; - if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), md) <= 0) - return Exception { OperationError }; + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), md) <= 0) + return Exception { OperationError }; + + if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), md) <= 0) + return Exception { OperationError }; + } if (!parameters.labelVector().isEmpty()) { size_t labelSize = parameters.labelVector().size(); @@ -80,18 +91,15 @@ ExceptionOr> CryptoAlgorithmRSA_OAEP::platformEncrypt(const Cryp cipherText.shrink(cipherTextLen); return cipherText; -#else - return Exception { NotSupportedError }; -#endif } ExceptionOr> CryptoAlgorithmRSA_OAEP::platformDecrypt(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector& cipherText) { -#if 1 // defined(EVP_PKEY_CTX_set_rsa_oaep_md) && defined(EVP_PKEY_CTX_set_rsa_mgf1_md) && defined(EVP_PKEY_CTX_set0_rsa_oaep_label) - const EVP_MD* md = digestAlgorithm(key.hashAlgorithmIdentifier()); - if (!md) - return Exception { NotSupportedError }; + return CryptoAlgorithmRSA_OAEP::platformDecryptWithHash(parameters, key, cipherText, key.hashAlgorithmIdentifier()); +} +ExceptionOr> CryptoAlgorithmRSA_OAEP::platformDecryptWithHash(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector& cipherText, CryptoAlgorithmIdentifier hashIdentifier) +{ auto ctx = EvpPKeyCtxPtr(EVP_PKEY_CTX_new(key.platformKey(), nullptr)); if (!ctx) return Exception { OperationError }; @@ -99,14 +107,25 @@ ExceptionOr> CryptoAlgorithmRSA_OAEP::platformDecrypt(const Cryp if (EVP_PKEY_decrypt_init(ctx.get()) <= 0) return Exception { OperationError }; - if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), RSA_PKCS1_OAEP_PADDING) <= 0) + auto padding = parameters.padding; + if (padding == 0) { + padding = RSA_PKCS1_OAEP_PADDING; + } + + if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) <= 0) return Exception { OperationError }; - if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), md) <= 0) - return Exception { OperationError }; + if (padding == RSA_PKCS1_OAEP_PADDING) { + const EVP_MD* md = digestAlgorithm(hashIdentifier); + if (!md) + return Exception { NotSupportedError }; - if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), md) <= 0) - return Exception { OperationError }; + if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx.get(), md) <= 0) + return Exception { OperationError }; + + if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx.get(), md) <= 0) + return Exception { OperationError }; + } if (!parameters.labelVector().isEmpty()) { size_t labelSize = parameters.labelVector().size(); @@ -129,9 +148,6 @@ ExceptionOr> CryptoAlgorithmRSA_OAEP::platformDecrypt(const Cryp plainText.shrink(plainTextLen); return plainText; -#else - return Exception { NotSupportedError }; -#endif } } // namespace WebCore diff --git a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRsaOaepParams.h b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRsaOaepParams.h index f960a38c67..4b58f7bb67 100644 --- a/src/bun.js/bindings/webcrypto/CryptoAlgorithmRsaOaepParams.h +++ b/src/bun.js/bindings/webcrypto/CryptoAlgorithmRsaOaepParams.h @@ -37,6 +37,7 @@ class CryptoAlgorithmRsaOaepParams final : public CryptoAlgorithmParameters { public: // Use labelVector() instead of label. The label will be gone once labelVector() is called. mutable std::optional label; + size_t padding = 0; // 0 represents the default value of the API Class parametersClass() const final { return Class::RsaOaepParams; } diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 8727f9a901..9b83b4f0b3 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -18,6 +18,8 @@ const { generateKeyPairSync, sign: nativeSign, verify: nativeVerify, + publicEncrypt, + privateDecrypt, } = $cpp("KeyObject.cpp", "createNodeCryptoBinding"); const { @@ -11610,16 +11612,10 @@ var require_privateDecrypt = __commonJS({ var require_browser10 = __commonJS({ "node_modules/public-encrypt/browser.js"(exports) { var publicEncrypt = require_publicEncrypt(); - exports.publicEncrypt = function (key, buf, options) { - return publicEncrypt(getKeyFrom(key, "public"), buf, options); - }; - var privateDecrypt = require_privateDecrypt(); - exports.privateDecrypt = function (key, buf, options) { - return privateDecrypt(getKeyFrom(key, "private"), buf, options); - }; exports.privateEncrypt = function (key, buf) { return publicEncrypt(getKeyFrom(key, "private"), buf, !0); }; + var privateDecrypt = require_privateDecrypt(); exports.publicDecrypt = function (key, buf) { return privateDecrypt(getKeyFrom(key, "public"), buf, !0); }; @@ -11721,10 +11717,8 @@ var require_crypto_browserify2 = __commonJS({ exports.Verify = sign.Verify; exports.createECDH = require_browser9(); var publicEncrypt = require_browser10(); - exports.publicEncrypt = publicEncrypt.publicEncrypt; exports.privateEncrypt = publicEncrypt.privateEncrypt; exports.publicDecrypt = publicEncrypt.publicDecrypt; - exports.privateDecrypt = publicEncrypt.privateDecrypt; exports.getRandomValues = values => crypto.getRandomValues(values); var rf = require_browser11(); exports.randomFill = rf.randomFill; @@ -12034,7 +12028,7 @@ function _createPublicKey(key) { } return KeyObject.from( createPublicKey({ - key: createPrivateKey({ key: actual_key, format: key.format, passphrase: key.passphrase }), + key: createPrivateKey({ key: actual_key, format: key.format || "pem", passphrase: key.passphrase }), format: "", }), ); @@ -12166,6 +12160,52 @@ crypto_exports.verify = function (algorithm, data, key, signature, callback) { } }; +// We are not allowed to call createPublicKey/createPrivateKey when we're already working with a +// KeyObject/CryptoKey of the same type (public/private). +function toCryptoKey(key, asPublic) { + // Top level CryptoKey. + if (key instanceof KeyObject || key instanceof CryptoKey) { + if (asPublic && key.type === "private") { + return _createPublicKey(key).$bunNativePtr; + } + return key.$bunNativePtr || key; + } + + // Nested CryptoKey. + if (key.key instanceof KeyObject || key.key instanceof CryptoKey) { + if (asPublic && key.key.type === "private") { + return _createPublicKey(key.key).$bunNativePtr; + } + return key.key.$bunNativePtr || key.key; + } + + // One of string, ArrayBuffer, Buffer, TypedArray, DataView, or Object. + return asPublic ? _createPublicKey(key).$bunNativePtr : _createPrivateKey(key).$bunNativePtr; +} + +function doAsymmetricCipher(key, message, operation, isEncrypt) { + // Our crypto bindings expect the key to be a `JSCryptoKey` property within an object. + const cryptoKey = toCryptoKey(key, isEncrypt); + const oaepLabel = + typeof key.oaepLabel === "string" ? Buffer.from(key.oaepLabel, key.encoding) : key.oaepLabel; + const keyObject = { + key: cryptoKey, + oaepHash: key.oaepHash, + oaepLabel, + padding: key.padding, + }; + const buffer = typeof message === "string" ? Buffer.from(message, key.encoding) : message; + return operation(keyObject, buffer); +} + +crypto_exports.publicEncrypt = function(key, message) { + return doAsymmetricCipher(key, message, publicEncrypt, true); +} + +crypto_exports.privateDecrypt = function(key, message) { + return doAsymmetricCipher(key, message, privateDecrypt, false); +} + __export(crypto_exports, { DEFAULT_ENCODING: () => DEFAULT_ENCODING, getRandomValues: () => getRandomValues, diff --git a/test/js/node/crypto/crypto-rsa.test.js b/test/js/node/crypto/crypto-rsa.test.js new file mode 100644 index 0000000000..716f693c6d --- /dev/null +++ b/test/js/node/crypto/crypto-rsa.test.js @@ -0,0 +1,405 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Copied from https://github.com/nodejs/node/blob/dcc2ed944f641004c0339bf76db58ccfefedd138/test/parallel/test-crypto-rsa-dsa.js + +const crypto = require("crypto"); +const constants = crypto.constants; +const fixtures = require("../test/common/fixtures"); + +// Test certificates +const certPem = fixtures.readKey("rsa_cert.crt"); +const keyPem = fixtures.readKey("rsa_private.pem"); +const rsaKeySize = 2048; +const rsaPubPem = fixtures.readKey("rsa_public.pem", "ascii"); +const rsaKeyPem = fixtures.readKey("rsa_private.pem", "ascii"); +const rsaKeyPemEncrypted = fixtures.readKey("rsa_private_encrypted.pem", "ascii"); +const rsaPkcs8KeyPem = fixtures.readKey("rsa_private_pkcs8.pem"); + +const ec = new TextEncoder(); + +const decryptError = { + message: expect.any(String), +}; + +function getBufferCopy(buf) { + return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); +} + +describe("RSA encryption/decryption", () => { + const input = "I AM THE WALRUS"; + const bufferToEncrypt = Buffer.from(input); + const bufferPassword = Buffer.from("password"); + + let encryptedBuffer; + let otherEncrypted; + + beforeAll(() => { + encryptedBuffer = crypto.publicEncrypt(rsaPubPem, bufferToEncrypt); + + const ab = getBufferCopy(ec.encode(rsaPubPem)); + const ab2enc = getBufferCopy(bufferToEncrypt); + + crypto.publicEncrypt(ab, ab2enc); + crypto.publicEncrypt(new Uint8Array(ab), new Uint8Array(ab2enc)); + crypto.publicEncrypt(new DataView(ab), new DataView(ab2enc)); + otherEncrypted = crypto.publicEncrypt( + { + key: Buffer.from(ab).toString("hex"), + encoding: "hex", + }, + Buffer.from(ab2enc).toString("hex"), + ); + }); + + test("privateDecrypt with rsaKeyPem", () => { + const decryptedBuffer = crypto.privateDecrypt(rsaKeyPem, encryptedBuffer); + expect(decryptedBuffer.toString()).toBe(input); + }); + + test("privateDecrypt with otherEncrypted", () => { + const otherDecrypted = crypto.privateDecrypt(rsaKeyPem, otherEncrypted); + expect(otherDecrypted.toString()).toBe(input); + }); + + test("privateDecrypt with rsaPkcs8KeyPem", () => { + const decryptedBuffer = crypto.privateDecrypt(rsaPkcs8KeyPem, encryptedBuffer); + expect(decryptedBuffer.toString()).toBe(input); + }); + + test("privateDecrypt with password", () => { + const decryptedBufferWithPassword = crypto.privateDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: "password", + }, + encryptedBuffer, + ); + expect(decryptedBufferWithPassword.toString()).toBe(input); + + const otherDecryptedBufferWithPassword = crypto.privateDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: ec.encode("password"), + }, + encryptedBuffer, + ); + expect(otherDecryptedBufferWithPassword.toString()).toBe(decryptedBufferWithPassword.toString()); + }); + + test("publicEncrypt and privateDecrypt with password", () => { + const encryptedBuffer = crypto.publicEncrypt( + { + key: rsaKeyPemEncrypted, + passphrase: "password", + }, + bufferToEncrypt, + ); + + const decryptedBufferWithPassword = crypto.privateDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: "password", + }, + encryptedBuffer, + ); + expect(decryptedBufferWithPassword.toString()).toBe(input); + }); + + test("privateEncrypt and publicDecrypt with buffer password", () => { + const encryptedBuffer = crypto.privateEncrypt( + { + key: rsaKeyPemEncrypted, + passphrase: bufferPassword, + }, + bufferToEncrypt, + ); + + const decryptedBufferWithPassword = crypto.publicDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: bufferPassword, + }, + encryptedBuffer, + ); + expect(decryptedBufferWithPassword.toString()).toBe(input); + }); + + test("privateEncrypt and publicDecrypt with RSA_PKCS1_PADDING", () => { + const encryptedBuffer = crypto.privateEncrypt( + { + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword, + }, + bufferToEncrypt, + ); + + const decryptedBufferWithPassword = crypto.publicDecrypt( + { + padding: crypto.constants.RSA_PKCS1_PADDING, + key: rsaKeyPemEncrypted, + passphrase: bufferPassword, + }, + encryptedBuffer, + ); + expect(decryptedBufferWithPassword.toString()).toBe(input); + + const decryptedBufferWithoutPadding = crypto.publicDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: bufferPassword, + }, + encryptedBuffer, + ); + expect(decryptedBufferWithoutPadding.toString()).toBe(input); + }); + + test("publicEncrypt and privateDecrypt with certPem and keyPem", () => { + const encryptedBuffer = crypto.publicEncrypt(certPem, bufferToEncrypt); + const decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + expect(decryptedBuffer.toString()).toBe(input); + }); + + test("publicEncrypt and privateDecrypt with keyPem", () => { + const encryptedBuffer = crypto.publicEncrypt(keyPem, bufferToEncrypt); + const decryptedBuffer = crypto.privateDecrypt(keyPem, encryptedBuffer); + expect(decryptedBuffer.toString()).toBe(input); + }); + + test("privateEncrypt and publicDecrypt with keyPem", () => { + const encryptedBuffer = crypto.privateEncrypt(keyPem, bufferToEncrypt); + const decryptedBuffer = crypto.publicDecrypt(keyPem, encryptedBuffer); + expect(decryptedBuffer.toString()).toBe(input); + }); + + test("privateDecrypt with wrong password", () => { + expect(() => { + crypto.privateDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: "wrong", + }, + bufferToEncrypt, + ); + }).toThrow(expect.objectContaining(decryptError)); + }); + + test("publicEncrypt with wrong password", () => { + expect(() => { + crypto.publicEncrypt( + { + key: rsaKeyPemEncrypted, + passphrase: "wrong", + }, + encryptedBuffer, + ); + }).toThrow(expect.objectContaining(decryptError)); + }); + + test("publicDecrypt with wrong password", () => { + const encryptedBuffer = crypto.privateEncrypt( + { + key: rsaKeyPemEncrypted, + passphrase: Buffer.from("password"), + }, + bufferToEncrypt, + ); + + expect(() => { + crypto.publicDecrypt( + { + key: rsaKeyPemEncrypted, + passphrase: Buffer.from("wrong"), + }, + encryptedBuffer, + ); + }).toThrow(expect.objectContaining(decryptError)); + }); +}); + +function test_rsa(padding, encryptOaepHash, decryptOaepHash, exceptionThrown) { + const size = padding === "RSA_NO_PADDING" ? rsaKeySize / 8 : 32; + const input = Buffer.allocUnsafe(size); + for (let i = 0; i < input.length; i++) input[i] = (i * 7 + 11) & 0xff; + const bufferToEncrypt = Buffer.from(input); + + padding = constants[padding]; + + const encryptedBuffer = crypto.publicEncrypt( + { + key: rsaPubPem, + padding: padding, + oaepHash: encryptOaepHash, + }, + bufferToEncrypt, + ); + + if (padding === constants.RSA_PKCS1_PADDING) { + expect(() => { + crypto.privateDecrypt( + { + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash, + }, + encryptedBuffer, + ); + }).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" })); + + expect(() => { + crypto.privateDecrypt( + { + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash, + }, + encryptedBuffer, + ); + }).toThrow(expect.objectContaining({ code: "ERR_INVALID_ARG_VALUE" })); + } else { + const decryptedBuffer = crypto.privateDecrypt( + { + key: rsaKeyPem, + padding: padding, + oaepHash: decryptOaepHash, + }, + encryptedBuffer, + ); + expect(decryptedBuffer).toEqual(input); + + const decryptedBufferPkcs8 = crypto.privateDecrypt( + { + key: rsaPkcs8KeyPem, + padding: padding, + oaepHash: decryptOaepHash, + }, + encryptedBuffer, + ); + expect(decryptedBufferPkcs8).toEqual(input); + } +} + +test(`RSA with RSA_NO_PADDING`, () => { + test_rsa("RSA_NO_PADDING"); +}); + +test(`RSA with RSA_PKCS1_PADDING`, () => { + test_rsa("RSA_PKCS1_PADDING"); +}); + +test(`RSA with RSA_PKCS1_OAEP_PADDING`, () => { + test_rsa("RSA_PKCS1_OAEP_PADDING"); + test_rsa("RSA_PKCS1_OAEP_PADDING", undefined, "sha1"); + test_rsa("RSA_PKCS1_OAEP_PADDING", "sha1", undefined); + test_rsa("RSA_PKCS1_OAEP_PADDING", "sha256", "sha256"); + test_rsa("RSA_PKCS1_OAEP_PADDING", "sha512", "sha512"); +}); + +test(`RSA with hash mismatch`, () => { + expect(() => { + test_rsa("RSA_PKCS1_OAEP_PADDING", "sha256", "sha512"); + }).toThrow(expect.objectContaining(decryptError)); +}); + +test("RSA-OAEP test vectors", () => { + const { decryptionTests } = JSON.parse(fixtures.readSync("rsa-oaep-test-vectors.js", "utf8")); + + for (const { ct, oaepHash, oaepLabel } of decryptionTests) { + const label = oaepLabel ? Buffer.from(oaepLabel, "hex") : undefined; + const copiedLabel = oaepLabel ? getBufferCopy(label) : undefined; + + const decrypted = crypto.privateDecrypt( + { + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: oaepLabel ? label : undefined, + }, + Buffer.from(ct, "hex"), + ); + + expect(decrypted.toString("utf8")).toBe("Hello Node.js"); + + const otherDecrypted = crypto.privateDecrypt( + { + key: rsaPkcs8KeyPem, + oaepHash, + oaepLabel: copiedLabel, + }, + Buffer.from(ct, "hex"), + ); + + expect(otherDecrypted.toString("utf8")).toBe("Hello Node.js"); + } +}); + +describe("Invalid oaepHash and oaepLabel options", () => { + const testCases = [ + { fn: crypto.publicEncrypt, name: "publicEncrypt", key: rsaPubPem }, + { fn: crypto.privateDecrypt, name: "privateDecrypt", key: rsaKeyPem }, + ]; + + testCases.forEach(({ fn, name, key }) => { + test(`${name} with invalid oaepHash`, () => { + expect(() => { + fn( + { + key, + oaepHash: "Hello world", + }, + Buffer.alloc(10), + ); + }).toThrow(expect.objectContaining({ + code: "ERR_CRYPTO_INVALID_DIGEST", + })); + + [0, false, null, Symbol(), () => {}].forEach(oaepHash => { + expect(() => { + fn( + { + key, + oaepHash, + }, + Buffer.alloc(10), + ); + }).toThrow(expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + })); + }); + }); + + test(`${name} with invalid oaepLabel`, () => { + [0, false, null, Symbol(), () => {}, {}].forEach(oaepLabel => { + expect(() => { + fn( + { + key, + oaepLabel, + }, + Buffer.alloc(10), + ); + }).toThrow(expect.objectContaining({ + code: "ERR_INVALID_ARG_TYPE", + })); + }); + }); + }); +});