Switch asymmetric encryption implementation to BoringSSL (#13786)

This commit is contained in:
Wilmer Paulino
2024-09-08 03:19:23 -07:00
committed by GitHub
parent 09cbb51c81
commit a0939ca4f1
7 changed files with 680 additions and 39 deletions

View File

@@ -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();

View File

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

View File

@@ -40,6 +40,9 @@ public:
static constexpr CryptoAlgorithmIdentifier s_identifier = CryptoAlgorithmIdentifier::RSA_OAEP;
static Ref<CryptoAlgorithm> create();
static ExceptionOr<Vector<uint8_t>> platformEncryptWithHash(const CryptoAlgorithmRsaOaepParams&, const CryptoKeyRSA&, const Vector<uint8_t>&, CryptoAlgorithmIdentifier hashIdentifier);
static ExceptionOr<Vector<uint8_t>> platformDecryptWithHash(const CryptoAlgorithmRsaOaepParams&, const CryptoKeyRSA&, const Vector<uint8_t>&, CryptoAlgorithmIdentifier hashIdentifier);
private:
CryptoAlgorithmRSA_OAEP() = default;
CryptoAlgorithmIdentifier identifier() const final;

View File

@@ -38,11 +38,11 @@ namespace WebCore {
ExceptionOr<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformEncrypt(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector<uint8_t>& 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<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformEncryptWithHash(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector<uint8_t>& plainText, CryptoAlgorithmIdentifier hashIdentifier)
{
auto ctx = EvpPKeyCtxPtr(EVP_PKEY_CTX_new(key.platformKey(), nullptr));
if (!ctx)
return Exception { OperationError };
@@ -50,14 +50,25 @@ ExceptionOr<Vector<uint8_t>> 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<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformEncrypt(const Cryp
cipherText.shrink(cipherTextLen);
return cipherText;
#else
return Exception { NotSupportedError };
#endif
}
ExceptionOr<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformDecrypt(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector<uint8_t>& 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<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformDecryptWithHash(const CryptoAlgorithmRsaOaepParams& parameters, const CryptoKeyRSA& key, const Vector<uint8_t>& cipherText, CryptoAlgorithmIdentifier hashIdentifier)
{
auto ctx = EvpPKeyCtxPtr(EVP_PKEY_CTX_new(key.platformKey(), nullptr));
if (!ctx)
return Exception { OperationError };
@@ -99,14 +107,25 @@ ExceptionOr<Vector<uint8_t>> 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<Vector<uint8_t>> CryptoAlgorithmRSA_OAEP::platformDecrypt(const Cryp
plainText.shrink(plainTextLen);
return plainText;
#else
return Exception { NotSupportedError };
#endif
}
} // namespace WebCore

View File

@@ -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<BufferSource::VariantType> label;
size_t padding = 0; // 0 represents the default value of the API
Class parametersClass() const final { return Class::RsaOaepParams; }

View File

@@ -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,

View File

@@ -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",
}));
});
});
});
});