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));
}