static EncodedJSValue doAsymmetricCipher()

in src/bun.js/bindings/KeyObject.cpp [2950:3081]


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<JSObject*>(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<JSCryptoKey*>(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<BufferSource::VariantType> 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<JSArrayBufferView*>(jsOaepLabelCell);
            oaepLabel = std::optional<BufferSource::VariantType>{jsBufferView->unsharedImpl()};
        } else if (jsOaepLabelType == ArrayBufferType) {
            auto* jsBuffer = jsDynamicCast<JSArrayBuffer*>(jsOaepLabelCell);
            oaepLabel = std::optional<BufferSource::VariantType>{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<CryptoKeyRSA>(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));
}