From a16f8a69dc5b2a6c3b820aaffd7a247d60df9a32 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Wed, 19 Jun 2024 05:10:43 -0400 Subject: [PATCH] feat: `subtle.sign() & verify()` support for `ECDSA` (#290) * javascript * c++ * tests wip * tests & fixes * typescript * typescript and android includes --- cpp/MGLKeys.cpp | 112 +-- cpp/MGLKeys.h | 3 +- cpp/Sig/MGLSignHostObjects.cpp | 705 +++++++----------- cpp/Sig/MGLSignHostObjects.h | 40 + cpp/webcrypto/MGLWebCrypto.cpp | 15 +- example/ios/Podfile.lock | 11 +- example/package.json | 2 + example/src/components/TestItem.tsx | 2 +- example/src/hooks/useTestList.ts | 1 + .../Tests/webcryptoTests/generateKey.ts | 40 +- .../Tests/webcryptoTests/sign_verify.ts | 167 +++++ example/yarn.lock | 28 +- implementation-coverage.md | 8 +- package.json | 3 +- src/Cipher.ts | 15 +- src/NativeQuickCrypto/sig.ts | 27 + src/NativeQuickCrypto/webcrypto.ts | 2 + src/ec.ts | 61 +- src/keys.ts | 14 +- src/subtle.ts | 86 ++- 20 files changed, 804 insertions(+), 538 deletions(-) create mode 100644 example/src/testing/Tests/webcryptoTests/sign_verify.ts diff --git a/cpp/MGLKeys.cpp b/cpp/MGLKeys.cpp index d8b0779c..9307431e 100644 --- a/cpp/MGLKeys.cpp +++ b/cpp/MGLKeys.cpp @@ -676,19 +676,25 @@ ManagedEVPPKey ManagedEVPPKey::GetPrivateKeyFromJs(jsi::Runtime& runtime, return GetParsedKey(runtime, std::move(pkey), ret, "Failed to read private key"); } else { - // CHECK(args[*offset]->IsObject() && allow_key_object); - // KeyObjectHandle* key; - // ASSIGN_OR_RETURN_UNWRAP(&key, args[*offset].As(), - // ManagedEVPPKey()); CHECK_EQ(key->Data()->GetKeyType(), - // kKeyTypePrivate); - // (*offset) += 4; - // return key->Data()->GetAsymmetricKey(); - throw jsi::JSError(runtime, "KeyObject are not currently supported"); + if( !(args[*offset].isObject() && allow_key_object)) { + throw jsi::JSError(runtime, + "ManagedEVPPKey::GetPrivateKeyFromJs: First argument " + "must be object (CryptoKey) and caller must pass " + "allow_key_object as true"); + } + std::shared_ptr handle = + std::static_pointer_cast( + args[*offset].asObject(runtime).getHostObject(runtime)); + CHECK_EQ(handle->Data()->GetKeyType(), kKeyTypePrivate); + (*offset) += 4; + return handle->Data()->GetAsymmetricKey(); } } ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs( - jsi::Runtime& runtime, const jsi::Value* args, unsigned int* offset) { + jsi::Runtime& runtime, + const jsi::Value* args, + unsigned int* offset) { if (args[*offset].asObject(runtime).isArrayBuffer(runtime)) { auto dataArrayBuffer = args[(*offset)++].asObject(runtime).getArrayBuffer(runtime); @@ -749,15 +755,19 @@ ManagedEVPPKey ManagedEVPPKey::GetPublicOrPrivateKeyFromJs( return ManagedEVPPKey::GetParsedKey(runtime, std::move(pkey), ret, "Failed to read asymmetric key"); } else { - throw jsi::JSError( - runtime, "public encrypt only supports ArrayBuffer at the moment"); - // CHECK(args[*offset]->IsObject()); - // KeyObjectHandle* key = - // Unwrap(args[*offset].As()); - // CHECK_NOT_NULL(key); - // CHECK_NE(key->Data()->GetKeyType(), kKeyTypeSecret); - // (*offset) += 4; - // return key->Data()->GetAsymmetricKey(); + if( !(args[*offset].isObject()) ) { + throw jsi::JSError(runtime, + "ManagedEVPPKey::GetPublicOrPrivateKeyFromJs: First " + "argument not ArrayBuffer or object (CryptoKey)"); + } + std::shared_ptr handle = + std::static_pointer_cast( + args[*offset].asObject(runtime).getHostObject(runtime)); + // note: below check is kKeyTypeSecret in Node.js + // but kKeyTypePublic in RNQC when testing verify() on ECDSA + CHECK_EQ(handle->Data()->GetKeyType(), kKeyTypePublic); + (*offset) += 4; + return handle->Data()->GetAsymmetricKey(); } } @@ -846,6 +856,8 @@ jsi::Value KeyObjectHandle::get( return this->InitJWK(rt); } else if (name == "keyDetail") { return this->GetKeyDetail(rt); + } else if (name == "getAsymmetricKeyType") { + return this->GetAsymmetricKeyType(rt); } return {}; @@ -1149,31 +1161,45 @@ jsi::Value KeyObjectHandle::GetKeyDetail(jsi::Runtime &rt) { }); } -// Local KeyObjectHandle::GetAsymmetricKeyType() const { -// const ManagedEVPPKey& key = data_->GetAsymmetricKey(); -// switch (EVP_PKEY_id(key.get())) { -// case EVP_PKEY_RSA: -// return env()->crypto_rsa_string(); -// case EVP_PKEY_RSA_PSS: -// return env()->crypto_rsa_pss_string(); -// case EVP_PKEY_DSA: -// return env()->crypto_dsa_string(); -// case EVP_PKEY_DH: -// return env()->crypto_dh_string(); -// case EVP_PKEY_EC: -// return env()->crypto_ec_string(); -// case EVP_PKEY_ED25519: -// return env()->crypto_ed25519_string(); -// case EVP_PKEY_ED448: -// return env()->crypto_ed448_string(); -// case EVP_PKEY_X25519: -// return env()->crypto_x25519_string(); -// case EVP_PKEY_X448: -// return env()->crypto_x448_string(); -// default: -// return Undefined(env()->isolate()); -// } -//} +jsi::Value KeyObjectHandle::GetAsymmetricKeyType(jsi::Runtime &rt) const { + return HOSTFN("getAsymmetricKeyType", 0) { + const ManagedEVPPKey& key = this->data_->GetAsymmetricKey(); + std::string ret; + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_RSA: + ret = "rss"; + break; + case EVP_PKEY_RSA_PSS: + ret = "rsa_pss"; + break; + case EVP_PKEY_DSA: + ret = "dsa"; + break; + case EVP_PKEY_DH: + ret = "dh"; + break; + case EVP_PKEY_EC: + ret = "ec"; + break; + case EVP_PKEY_ED25519: + ret = "ed25519"; + break; + case EVP_PKEY_ED448: + ret = "ed448"; + break; + case EVP_PKEY_X25519: + ret = "x25519"; + break; + case EVP_PKEY_X448: + ret = "x448"; + break; + default: + throw jsi::JSError(rt, "unknown KeyType in GetAsymmetricKeyType"); + } + return jsi::String::createFromUtf8(rt, ret); + }); +} + // // void KeyObjectHandle::GetAsymmetricKeyType( // const diff --git a/cpp/MGLKeys.h b/cpp/MGLKeys.h index 3a2fd380..55b1140f 100644 --- a/cpp/MGLKeys.h +++ b/cpp/MGLKeys.h @@ -181,10 +181,11 @@ class JSI_EXPORT KeyObjectHandle: public jsi::HostObject { jsi::Runtime& rt, const PrivateKeyEncodingConfig& config) const; jsi::Value ExportSecretKey(jsi::Runtime& rt) const; + jsi::Value GetKeyDetail(jsi::Runtime &rt); + jsi::Value GetAsymmetricKeyType(jsi::Runtime &rt) const; jsi::Value Init(jsi::Runtime &rt); jsi::Value InitECRaw(jsi::Runtime &rt); jsi::Value InitJWK(jsi::Runtime &rt); - jsi::Value GetKeyDetail(jsi::Runtime &rt); private: std::shared_ptr data_; diff --git a/cpp/Sig/MGLSignHostObjects.cpp b/cpp/Sig/MGLSignHostObjects.cpp index c14aa941..8ca9c276 100644 --- a/cpp/Sig/MGLSignHostObjects.cpp +++ b/cpp/Sig/MGLSignHostObjects.cpp @@ -115,26 +115,24 @@ unsigned int GetBytesOfRS(const ManagedEVPPKey& pkey) { return (bits + 7) / 8; } -// -// bool ExtractP1363( -// const unsigned char* sig_data, -// unsigned char* out, -// size_t len, -// size_t n) { -// ECDSASigPointer asn1_sig(d2i_ECDSA_SIG(nullptr, -// &sig_data, len)); if (!asn1_sig) -// return false; -// -// const BIGNUM* pr = ECDSA_SIG_get0_r(asn1_sig.get()); -// const BIGNUM* ps = ECDSA_SIG_get0_s(asn1_sig.get()); -// -// return BN_bn2binpad(pr, out, n) > 0 && BN_bn2binpad(ps, -// out + n, n) > 0; -// } -// -// // Returns the maximum size of each of the integers (r, s) of the DSA -// signature. std::unique_ptr -// ConvertSignatureToP1363(Environment* env, + + bool ExtractP1363(const unsigned char* sig_data, + unsigned char* out, + size_t len, + size_t n) { + ECDSASigPointer asn1_sig(d2i_ECDSA_SIG(nullptr, + &sig_data, len)); if (!asn1_sig) + return false; + + const BIGNUM* pr = ECDSA_SIG_get0_r(asn1_sig.get()); + const BIGNUM* ps = ECDSA_SIG_get0_s(asn1_sig.get()); + + return BN_bn2binpad(pr, out, n) > 0 && BN_bn2binpad(ps, + out + n, n) > 0; +} + +// // Returns the maximum size of each of the integers (r, s) of the DSA signature. +// std::unique_ptr ConvertSignatureToP1363(Environment* env, // const ManagedEVPPKey& // pkey, // std::unique_ptr&& @@ -155,30 +153,28 @@ unsigned int GetBytesOfRS(const ManagedEVPPKey& pkey) { // // return buf; // } -// -// // Returns the maximum size of each of the integers (r, s) of the DSA -// signature. ByteSource ConvertSignatureToP1363( -// Environment* env, -// const ManagedEVPPKey& pkey, -// const ByteSource& signature) { -// unsigned int n = GetBytesOfRS(pkey); -// if (n == kNoDsaSignature) -// return ByteSource(); -// -// const unsigned char* sig_data = -// signature.data(); -// -// ByteSource::Builder out(n * 2); -// memset(out.data(), 0, n * 2); -// -// if (!ExtractP1363(sig_data, -// out.data(), -// signature.size(), n)) -// return ByteSource(); -// -// return std::move(out).release(); -// } -// + + // Returns the maximum size of each of the integers (r, s) of the DSA signature. + ByteSource ConvertSignatureToP1363(const ManagedEVPPKey& pkey, + const ByteSource& signature) { + unsigned int n = GetBytesOfRS(pkey); + if (n == kNoDsaSignature) + return ByteSource(); + + const unsigned char* sig_data = + signature.data(); + + ByteSource::Builder out(n * 2); + memset(out.data(), 0, n * 2); + + if (!ExtractP1363(sig_data, + out.data(), + signature.size(), n)) + return ByteSource(); + + return std::move(out).release(); +} + ByteSource ConvertSignatureToDER(const ManagedEVPPKey& pkey, ByteSource&& out) { unsigned int n = GetBytesOfRS(pkey); if (n == kNoDsaSignature) return std::move(out); @@ -253,27 +249,27 @@ ByteSource ConvertSignatureToDER(const ManagedEVPPKey& pkey, ByteSource&& out) { // return; // } // } -// -// bool IsOneShot(const ManagedEVPPKey& key) { -// switch (EVP_PKEY_id(key.get())) { -// case EVP_PKEY_ED25519: -// case EVP_PKEY_ED448: -// return true; -// default: -// return false; -// } -// } -// -// bool UseP1363Encoding(const ManagedEVPPKey& key, -// const DSASigEnc& dsa_encoding) { -// switch (EVP_PKEY_id(key.get())) { -// case EVP_PKEY_EC: -// case EVP_PKEY_DSA: -// return dsa_encoding == kSigEncP1363; -// default: -// return false; -// } -// } + + bool IsOneShot(const ManagedEVPPKey& key) { + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_ED25519: + case EVP_PKEY_ED448: + return true; + default: + return false; + } + } + + bool UseP1363Encoding(const ManagedEVPPKey& key, + const DSASigEnc& dsa_encoding) { + switch (EVP_PKEY_id(key.get())) { + case EVP_PKEY_EC: + case EVP_PKEY_DSA: + return dsa_encoding == kSigEncP1363; + default: + return false; + } + } SignBase::SignResult SignBase::SignFinal(jsi::Runtime& runtime, const ManagedEVPPKey& pkey, @@ -498,137 +494,6 @@ void SignBase::InstallMethods(mode mode) { } } -// Verify::Verify(Environment* env, Local wrap) -//: SignBase(env, wrap) { -// MakeWeak(); -//} -// -// void Verify::Initialize(Environment* env, Local target) { -// Local t = env->NewFunctionTemplate(New); -// -// t->InstanceTemplate()->SetInternalFieldCount( -// SignBase::kInternalFieldCount); -// t->Inherit(BaseObject::GetConstructorTemplate(env)); -// -// env->SetProtoMethod(t, "init", VerifyInit); -// env->SetProtoMethod(t, "update", VerifyUpdate); -// env->SetProtoMethod(t, "verify", VerifyFinal); -// -// env->SetConstructorFunction(target, "Verify", t); -//} -// -// void Verify::RegisterExternalReferences(ExternalReferenceRegistry* registry) -// { -// registry->Register(New); -// registry->Register(VerifyInit); -// registry->Register(VerifyUpdate); -// registry->Register(VerifyFinal); -//} -// -// void Verify::New(const FunctionCallbackInfo& args) { -// Environment* env = Environment::GetCurrent(args); -// new Verify(env, args.This()); -//} -// -// void Verify::VerifyInit(const FunctionCallbackInfo& args) { -// Environment* env = Environment::GetCurrent(args); -// Verify* verify; -// ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); -// -// const node::Utf8Value verify_type(args.GetIsolate(), args[0]); -// crypto::CheckThrow(env, verify->Init(*verify_type)); -//} -// -// void Verify::VerifyUpdate(const FunctionCallbackInfo& args) { -// Decode(args, [](Verify* verify, -// const FunctionCallbackInfo& args, -// const char* data, size_t size) { -// Environment* env = Environment::GetCurrent(args); -// if (UNLIKELY(size > INT_MAX)) -// return THROW_ERR_OUT_OF_RANGE(env, "data is too long"); -// Error err = verify->Update(data, size); -// crypto::CheckThrow(verify->env(), err); -// }); -//} -// -// SignBase::Error Verify::VerifyFinal(const ManagedEVPPKey& pkey, -// const ByteSource& sig, -// int padding, -// const Maybe& saltlen, -// bool* verify_result) { -// if (!mdctx_) -// return kSignNotInitialised; -// -// unsigned char m[EVP_MAX_MD_SIZE]; -// unsigned int m_len; -// *verify_result = false; -// EVPMDPointer mdctx = std::move(mdctx_); -// -// if (!EVP_DigestFinal_ex(mdctx.get(), m, &m_len)) -// return kSignPublicKey; -// -// EVPKeyCtxPointer pkctx(EVP_PKEY_CTX_new(pkey.get(), nullptr)); -// if (pkctx && -// EVP_PKEY_verify_init(pkctx.get()) > 0 && -// ApplyRSAOptions(pkey, pkctx.get(), padding, saltlen) && -// EVP_PKEY_CTX_set_signature_md(pkctx.get(), -// EVP_MD_CTX_md(mdctx.get())) > 0) { -// const unsigned char* s = sig.data(); -// const int r = EVP_PKEY_verify(pkctx.get(), s, sig.size(), m, m_len); -// *verify_result = r == 1; -// } -// -// return kSignOk; -//} -// -// void Verify::VerifyFinal(const FunctionCallbackInfo& args) { -// Environment* env = Environment::GetCurrent(args); -// ClearErrorOnReturn clear_error_on_return; -// -// Verify* verify; -// ASSIGN_OR_RETURN_UNWRAP(&verify, args.Holder()); -// -// unsigned int offset = 0; -// ManagedEVPPKey pkey = -// ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &offset); -// if (!pkey) -// return; -// -// ArrayBufferOrViewContents hbuf(args[offset]); -// if (UNLIKELY(!hbuf.CheckSizeInt32())) -// return THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); -// -// int padding = GetDefaultSignPadding(pkey); -// if (!args[offset + 1]->IsUndefined()) { -// CHECK(args[offset + 1]->IsInt32()); -// padding = args[offset + 1].As()->Value(); -// } -// -// Maybe salt_len = Nothing(); -// if (!args[offset + 2]->IsUndefined()) { -// CHECK(args[offset + 2]->IsInt32()); -// salt_len = Just(args[offset + 2].As()->Value()); -// } -// -// CHECK(args[offset + 3]->IsInt32()); -// DSASigEnc dsa_sig_enc = -// static_cast(args[offset + 3].As()->Value()); -// -// ByteSource signature = hbuf.ToByteSource(); -// if (dsa_sig_enc == kSigEncP1363) { -// signature = ConvertSignatureToDER(pkey, hbuf.ToByteSource()); -// if (signature.data() == nullptr) -// return crypto::CheckThrow(env, Error::kSignMalformedSignature); -// } -// -// bool verify_result; -// Error err = verify->VerifyFinal(pkey, signature, padding, -// salt_len, &verify_result); -// if (err != kSignOk) -// return crypto::CheckThrow(env, err); -// args.GetReturnValue().Set(verify_result); -//} -// // SignConfiguration::SignConfiguration(SignConfiguration&& other) noexcept //: job_mode(other.job_mode), // mode(other.mode), @@ -658,230 +523,228 @@ void SignBase::InstallMethods(mode mode) { // tracker->TrackFieldWithSize("signature", signature.size()); // } // } -// -// Maybe SignTraits::AdditionalConfig( -// CryptoJobMode mode, -// const FunctionCallbackInfo& -// args, unsigned int offset, -// SignConfiguration* params) { -// ClearErrorOnReturn clear_error_on_return; -// Environment* env = Environment::GetCurrent(args); -// -// params->job_mode = mode; -// -// CHECK(args[offset]->IsUint32()); // Sign Mode -// -// params->mode = -// static_cast(args[offset].As()->Value()); -// -// ManagedEVPPKey key; -// unsigned int keyParamOffset = offset + 1; -// if (params->mode == SignConfiguration::kVerify) { -// key = ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(args, &keyParamOffset); -// } else { -// key = ManagedEVPPKey::GetPrivateKeyFromJs(args, &keyParamOffset, true); -// } -// if (!key) -// return Nothing(); -// params->key = key; -// -// ArrayBufferOrViewContents data(args[offset + 5]); -// if (UNLIKELY(!data.CheckSizeInt32())) { -// THROW_ERR_OUT_OF_RANGE(env, "data is too big"); -// return Nothing(); -// } -// params->data = mode == kCryptoJobAsync -// ? data.ToCopy() -// : data.ToByteSource(); -// -// if (args[offset + 6]->IsString()) { -// Utf8Value digest(env->isolate(), args[offset + 6]); -// params->digest = EVP_get_digestbyname(*digest); -// if (params->digest == nullptr) { -// THROW_ERR_CRYPTO_INVALID_DIGEST(env); -// return Nothing(); -// } -// } -// -// if (args[offset + 7]->IsInt32()) { // Salt length -// params->flags |= SignConfiguration::kHasSaltLength; -// params->salt_length = args[offset + 7].As()->Value(); -// } -// if (args[offset + 8]->IsUint32()) { // Padding -// params->flags |= SignConfiguration::kHasPadding; -// params->padding = args[offset + 8].As()->Value(); -// } -// -// if (args[offset + 9]->IsUint32()) { // DSA Encoding -// params->dsa_encoding = -// static_cast(args[offset + 9].As()->Value()); -// if (params->dsa_encoding != kSigEncDER && -// params->dsa_encoding != kSigEncP1363) { -// THROW_ERR_OUT_OF_RANGE(env, "invalid signature encoding"); -// return Nothing(); -// } -// } -// -// if (params->mode == SignConfiguration::kVerify) { -// ArrayBufferOrViewContents signature(args[offset + 10]); -// if (UNLIKELY(!signature.CheckSizeInt32())) { -// THROW_ERR_OUT_OF_RANGE(env, "signature is too big"); -// return Nothing(); -// } -// // If this is an EC key (assuming ECDSA) we need to convert the -// // the signature from WebCrypto format into DER format... -// ManagedEVPPKey m_pkey = params->key; -// Mutex::ScopedLock lock(*m_pkey.mutex()); -// if (UseP1363Encoding(m_pkey, params->dsa_encoding)) { -// params->signature = -// ConvertSignatureToDER(m_pkey, signature.ToByteSource()); -// } else { -// params->signature = mode == kCryptoJobAsync -// ? signature.ToCopy() -// : signature.ToByteSource(); -// } -// } -// -// return Just(true); -// } -// -// bool SignTraits::DeriveBits( -// Environment* env, -// const SignConfiguration& params, -// ByteSource* out) { -// ClearErrorOnReturn clear_error_on_return; -// EVPMDPointer context(EVP_MD_CTX_new()); -// EVP_PKEY_CTX* ctx = nullptr; -// -// switch (params.mode) { -// case SignConfiguration::kSign: -// if (!EVP_DigestSignInit( -// context.get(), -// &ctx, -// params.digest, -// nullptr, -// params.key.get())) { -// crypto::CheckThrow(env, -// SignBase::Error::kSignInit); return false; -// } -// break; -// case SignConfiguration::kVerify: -// if (!EVP_DigestVerifyInit( -// context.get(), -// &ctx, -// params.digest, -// nullptr, -// params.key.get())) { -// crypto::CheckThrow(env, -// SignBase::Error::kSignInit); return false; -// } -// break; -// } -// -// int padding = params.flags & SignConfiguration::kHasPadding -// ? params.padding -// : GetDefaultSignPadding(params.key); -// -// Maybe salt_length = params.flags & SignConfiguration::kHasSaltLength -// ? Just(params.salt_length) : Nothing(); -// -// if (!ApplyRSAOptions( -// params.key, -// ctx, -// padding, -// salt_length)) { -// crypto::CheckThrow(env, -// SignBase::Error::kSignPrivateKey); return false; -// } -// -// switch (params.mode) { -// case SignConfiguration::kSign: { -// if (IsOneShot(params.key)) { -// size_t len; -// if (!EVP_DigestSign( -// context.get(), -// nullptr, -// &len, -// params.data.data(), -// params.data.size())) { -// crypto::CheckThrow(env, -// SignBase::Error::kSignPrivateKey); return -// false; -// } -// ByteSource::Builder buf(len); -// if (!EVP_DigestSign(context.get(), -// buf.data(), -// &len, -// params.data.data(), -// params.data.size())) { -// crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); -// return false; -// } -// *out = std::move(buf).release(len); -// } else { -// size_t len; -// if (!EVP_DigestSignUpdate( -// context.get(), -// params.data.data(), -// params.data.size()) || -// !EVP_DigestSignFinal(context.get(), nullptr, &len)) { -// crypto::CheckThrow(env, SignBase::Error::kSignPrivateKey); -// return false; -// } -// ByteSource::Builder buf(len); -// if (!EVP_DigestSignFinal( -// context.get(), buf.data(), -// &len)) { -// crypto::CheckThrow(env, -// SignBase::Error::kSignPrivateKey); return -// false; -// } -// -// if (UseP1363Encoding(params.key, params.dsa_encoding)) { -// *out = ConvertSignatureToP1363( -// env, params.key, -// std::move(buf).release()); -// } else { -// *out = std::move(buf).release(len); -// } -// } -// break; -// } -// case SignConfiguration::kVerify: { -// ByteSource::Builder buf(1); -// buf.data()[0] = 0; -// if (EVP_DigestVerify( -// context.get(), -// params.signature.data(), -// params.signature.size(), -// params.data.data(), -// params.data.size()) == 1) { -// buf.data()[0] = 1; -// } -// *out = std::move(buf).release(); -// } -// } -// -// return true; -// } -// -// Maybe SignTraits::EncodeOutput( -// Environment* env, -// const SignConfiguration& params, -// ByteSource* out, -// Local* result) { -// switch (params.mode) { -// case SignConfiguration::kSign: -// *result = out->ToArrayBuffer(env); -// break; -// case SignConfiguration::kVerify: -// *result = out->data()[0] == 1 ? v8::True(env->isolate()) -// : v8::False(env->isolate()); -// break; -// default: -// UNREACHABLE(); -// } -// return Just(!result->IsEmpty()); -// } + +SignConfiguration SubtleSignVerify::GetParamsFromJS(jsi::Runtime &rt, + const jsi::Value *args) { + SignConfiguration params; + unsigned int offset = 0; + + // mode (sign/verify) + params.mode = static_cast((int)args[offset].getNumber()); + offset++; + + // key + ManagedEVPPKey key; + unsigned int keyParamOffset = offset; + if (params.mode == SignConfiguration::kVerify) { + key = ManagedEVPPKey::GetPublicOrPrivateKeyFromJs(rt, args, &keyParamOffset); + } else { + key = ManagedEVPPKey::GetPrivateKeyFromJs(rt, args, &keyParamOffset, true); + } + if (!key) { + return params; + } + params.key = key; + offset = 5; + + // data + if (!args[offset].isObject() || !args[offset].asObject(rt).isArrayBuffer(rt)) { + throw jsi::JSError(rt, "data is not an array buffer"); + return params; + } + ByteSource data = ByteSource::FromStringOrBuffer(rt, args[offset]); + if (data.size() > INT_MAX) { + throw jsi::JSError(rt, "data is too big (> int32)"); + return params; + } + params.data = std::move(data); + offset++; + + // digest + if (args[offset].isString()) { + std::string digest = args[offset].asString(rt).utf8(rt); + params.digest = EVP_get_digestbyname(digest.c_str()); + if (params.digest == nullptr) { + throw jsi::JSError(rt, "invalid digest"); + return params; + } + } + offset++; + + // salt length + if (CheckIsInt32(args[offset])) { + params.flags |= SignConfiguration::kHasSaltLength; + params.salt_length = args[offset].asNumber(); + } + offset++; + + // padding + if (CheckIsInt32(args[offset])) { + params.flags |= SignConfiguration::kHasPadding; + params.padding = args[offset].asNumber(); + + } + offset++; + + // dsa encoding + if (args[offset].isNumber()) { + params.dsa_encoding = + static_cast(args[offset].asNumber()); + if (params.dsa_encoding != kSigEncDER && + params.dsa_encoding != kSigEncP1363) { + throw jsi::JSError(rt, "invalid signature encoding"); + return params; + } + } + offset++; + + // signature + if (params.mode == SignConfiguration::kVerify) { + ByteSource signature = ByteSource::FromStringOrBuffer(rt, args[offset]); + if (signature.size() > INT_MAX) { + throw jsi::JSError(rt, "signature is too big (> int32)"); + return params; + } + // If this is an EC key (assuming ECDSA) we need to convert the + // the signature from WebCrypto format into DER format... + ManagedEVPPKey m_pkey = params.key; + // Mutex::ScopedLock lock(*m_pkey.mutex()); + if (UseP1363Encoding(m_pkey, params.dsa_encoding)) { + params.signature = + ConvertSignatureToDER(m_pkey, std::move(signature)); + } else { + params.signature = std::move(signature); + } + } + + return params; +} + +// Subtle Sign/Verify + +void SubtleSignVerify::DoSignVerify(jsi::Runtime &rt, + const SignConfiguration ¶ms, + ByteSource &out) { + + EVPMDPointer context(EVP_MD_CTX_new()); + EVP_PKEY_CTX* ctx = nullptr; + + switch (params.mode) { + case SignConfiguration::kSign: + if (!EVP_DigestSignInit( + context.get(), + &ctx, + params.digest, + nullptr, + params.key.get())) { + throw jsi::JSError(rt, "EVP_DigestSignInit failed"); + } + break; + case SignConfiguration::kVerify: + if (!EVP_DigestVerifyInit( + context.get(), + &ctx, + params.digest, + nullptr, + params.key.get())) { + throw jsi::JSError(rt, "EVP_DigestVerifyInit failed"); + } + break; + } + + int padding = params.flags & SignConfiguration::kHasPadding + ? params.padding + : GetDefaultSignPadding(params.key); + + std::optional salt_length = params.flags & SignConfiguration::kHasSaltLength + ? std::optional(params.salt_length) : std::nullopt; + + if (!ApplyRSAOptions(params.key, + ctx, + padding, + salt_length)) { + throw jsi::JSError(rt, "PEM_read_bio_PrivateKey failed"); + } + + switch (params.mode) { + case SignConfiguration::kSign: { + if (IsOneShot(params.key)) { + size_t len; + if (!EVP_DigestSign( + context.get(), + nullptr, + &len, + params.data.data(), + params.data.size())) { + throw jsi::JSError(rt, "PEM_read_bio_PrivateKey failed"); + } + ByteSource::Builder buf(len); + if (!EVP_DigestSign(context.get(), + buf.data(), + &len, + params.data.data(), + params.data.size())) { + throw jsi::JSError(rt, "PEM_read_bio_PrivateKey failed"); + } + out = std::move(buf).release(len); + } else { + size_t len; + if (!EVP_DigestSignUpdate( + context.get(), + params.data.data(), + params.data.size()) || + !EVP_DigestSignFinal(context.get(), nullptr, &len)) { + throw jsi::JSError(rt, "PEM_read_bio_PrivateKey failed"); + } + ByteSource::Builder buf(len); + if (!EVP_DigestSignFinal( + context.get(), buf.data(), + &len)) { + throw jsi::JSError(rt, "PEM_read_bio_PrivateKey failed"); + } + + if (UseP1363Encoding(params.key, params.dsa_encoding)) { + out = ConvertSignatureToP1363(params.key, + std::move(buf).release()); + } else { + out = std::move(buf).release(len); + } + } + break; + } + case SignConfiguration::kVerify: { + ByteSource::Builder buf(1); + buf.data()[0] = 0; + if (EVP_DigestVerify( + context.get(), + params.signature.data(), + params.signature.size(), + params.data.data(), + params.data.size()) == 1) { + buf.data()[0] = 1; + } + out = std::move(buf).release(); + } + } + + // return out; +} + +jsi::Value SubtleSignVerify::EncodeOutput(jsi::Runtime &rt, + const SignConfiguration ¶ms, + ByteSource &output) { + jsi::Value result; + switch (params.mode) { + case SignConfiguration::kSign: + result = toJSI(rt, std::move(output)); + break; + case SignConfiguration::kVerify: + result = jsi::Value(output.data()[0] == 1); + break; + default: + throw jsi::JSError(rt, "unreachable code in SubtleSignVerify::EncodeOutput"); + } + return result; +} } // namespace margelo diff --git a/cpp/Sig/MGLSignHostObjects.h b/cpp/Sig/MGLSignHostObjects.h index 70a7c648..c0780158 100644 --- a/cpp/Sig/MGLSignHostObjects.h +++ b/cpp/Sig/MGLSignHostObjects.h @@ -69,6 +69,46 @@ class SignBase : public MGLSmartHostObject { EVPMDPointer mdctx_; }; +struct SignConfiguration final { // : public MemoryRetainer + enum Mode { + kSign, + kVerify + }; + enum Flags { + kHasNone = 0, + kHasSaltLength = 1, + kHasPadding = 2 + }; + + // CryptoJobMode job_mode; // all async for now + Mode mode; + ManagedEVPPKey key; + ByteSource data; + ByteSource signature; + const EVP_MD* digest = nullptr; + int flags = SignConfiguration::kHasNone; + int padding = 0; + int salt_length = 0; + DSASigEnc dsa_encoding = kSigEncDER; + + SignConfiguration() = default; + + // explicit SignConfiguration(SignConfiguration&& other) noexcept; + + // SignConfiguration& operator=(SignConfiguration&& other) noexcept; + + // void MemoryInfo(MemoryTracker* tracker) const override; + // SET_MEMORY_INFO_NAME(SignConfiguration) + // SET_SELF_SIZE(SignConfiguration) +}; + +class SubtleSignVerify { + public: + SignConfiguration GetParamsFromJS(jsi::Runtime &rt, const jsi::Value *args); + void DoSignVerify(jsi::Runtime &rt, const SignConfiguration ¶ms, ByteSource &out); + jsi::Value EncodeOutput(jsi::Runtime &rt,const SignConfiguration ¶ms, ByteSource &out); +}; + class MGLSignHostObject : public SignBase { public: explicit MGLSignHostObject( diff --git a/cpp/webcrypto/MGLWebCrypto.cpp b/cpp/webcrypto/MGLWebCrypto.cpp index 9a15723e..ebccf0dd 100644 --- a/cpp/webcrypto/MGLWebCrypto.cpp +++ b/cpp/webcrypto/MGLWebCrypto.cpp @@ -13,11 +13,13 @@ #ifdef ANDROID #include "JSIUtils/MGLJSIMacros.h" -#include "webcrypto/crypto_ec.h" +#include "Sig/MGLSignHostObjects.h" #include "Utils/MGLUtils.h" +#include "webcrypto/crypto_ec.h" #else -#include "MGLUtils.h" #include "MGLJSIMacros.h" +#include "MGLSignHostObjects.h" +#include "MGLUtils.h" #include "crypto_ec.h" #endif @@ -51,10 +53,19 @@ jsi::Value createWebCryptoObject(jsi::Runtime &rt) { return toJSI(rt, std::move(out)); }); + auto signVerify = HOSTFN("signVerify", 4) { + auto ssv = SubtleSignVerify(); + auto params = ssv.GetParamsFromJS(rt, args); + ByteSource out; + ssv.DoSignVerify(rt, params, out); + return ssv.EncodeOutput(rt, params, out); + }); + obj.setProperty(rt, "createKeyObjectHandle", std::move(createKeyObjectHandle)); obj.setProperty(rt, "ecExportKey", std::move(ecExportKey)); + obj.setProperty(rt, "signVerify", std::move(signVerify)); return obj; }; diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0c1ad8b2..4724bb36 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -317,10 +317,13 @@ PODS: - React-jsinspector (0.72.7) - React-logger (0.72.7): - glog + - react-native-fast-encoder (0.1.12): + - RCT-Folly (= 2021.07.22.00) + - React-Core - react-native-quick-base64 (2.1.2): - RCT-Folly (= 2021.07.22.00) - React-Core - - react-native-quick-crypto (0.7.0-rc.6): + - react-native-quick-crypto (0.7.0-rc.9): - OpenSSL-Universal - RCT-Folly (= 2021.07.22.00) - React @@ -470,6 +473,7 @@ DEPENDENCIES: - React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`) - React-logger (from `../node_modules/react-native/ReactCommon/logger`) + - react-native-fast-encoder (from `../node_modules/react-native-fast-encoder`) - react-native-quick-base64 (from `../node_modules/react-native-quick-base64`) - react-native-quick-crypto (from `../..`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) @@ -546,6 +550,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/jsinspector" React-logger: :path: "../node_modules/react-native/ReactCommon/logger" + react-native-fast-encoder: + :path: "../node_modules/react-native-fast-encoder" react-native-quick-base64: :path: "../node_modules/react-native-quick-base64" react-native-quick-crypto: @@ -619,8 +625,9 @@ SPEC CHECKSUMS: React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f + react-native-fast-encoder: 6f59e9b08e2bc5a8bf1f36e1630cdcfd66dd18af react-native-quick-base64: 61228d753294ae643294a75fece8e0e80b7558a6 - react-native-quick-crypto: 0f1c9ae20bc06e10f0349887a9e3767ff683c5ac + react-native-quick-crypto: 8e4521904e2b9da9454bcd825e9c198a1efb3f29 react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a diff --git a/example/package.json b/example/package.json index 7dc13eef..7a1ed13b 100644 --- a/example/package.json +++ b/example/package.json @@ -25,6 +25,7 @@ "mocha": "^10.2.0", "react": "18.2.0", "react-native": "0.72.7", + "react-native-fast-encoder": "^0.1.12", "react-native-quick-base64": "^2.1.2", "react-native-safe-area-context": "^4.5.0", "react-native-screens": "^3.20.0", @@ -43,6 +44,7 @@ "@types/jest": "^29.2.1", "@types/mocha": "^10.0.1", "@types/react": "^18.0.24", + "@types/react-native": "^0.72.7", "@types/react-test-renderer": "^18.0.0", "@types/readable-stream": "^4.0.11", "babel-plugin-module-resolver": "^5.0.0", diff --git a/example/src/components/TestItem.tsx b/example/src/components/TestItem.tsx index bbb8b479..968ac6e6 100644 --- a/example/src/components/TestItem.tsx +++ b/example/src/components/TestItem.tsx @@ -74,7 +74,7 @@ export const TestItem: React.FC = ({ const styles = StyleSheet.create({ container: { width: '100%', - paddingVertical: 5, + paddingVertical: 2, flexDirection: 'row', alignContent: 'center', alignItems: 'center', diff --git a/example/src/hooks/useTestList.ts b/example/src/hooks/useTestList.ts index 69c3108a..1a806861 100644 --- a/example/src/hooks/useTestList.ts +++ b/example/src/hooks/useTestList.ts @@ -19,6 +19,7 @@ import '../testing/Tests/webcryptoTests/deriveBits'; import '../testing/Tests/webcryptoTests/digest'; import '../testing/Tests/webcryptoTests/generateKey'; import '../testing/Tests/webcryptoTests/import_export'; +import '../testing/Tests/webcryptoTests/sign_verify'; export const useTestList = (): [ Suites, diff --git a/example/src/testing/Tests/webcryptoTests/generateKey.ts b/example/src/testing/Tests/webcryptoTests/generateKey.ts index c119bd04..a7197675 100644 --- a/example/src/testing/Tests/webcryptoTests/generateKey.ts +++ b/example/src/testing/Tests/webcryptoTests/generateKey.ts @@ -427,11 +427,6 @@ describe('subtle - generateKey', () => { } */ - type CryptoKeysInPair = { - publicKey: CryptoKey; - privateKey: CryptoKey; - }; - // Test EC Key Generation { async function testECKeyGen( @@ -454,23 +449,24 @@ describe('subtle - generateKey', () => { true, usages ); - const { publicKey, privateKey }: CryptoKeysInPair = - pair as CryptoKeyPair; - - expect(publicKey).is.not.undefined; - expect(privateKey).is.not.undefined; - expect(isCryptoKey(publicKey)); - expect(isCryptoKey(privateKey)); - expect(publicKey.type).to.equal('public'); - expect(privateKey.type).to.equal('private'); - expect(publicKey.keyExtractable).to.equal(true); - expect(privateKey.keyExtractable).to.equal(true); - expect(publicKey.keyUsages).to.deep.equal(publicUsages); - expect(privateKey.keyUsages).to.deep.equal(privateUsages); - expect(publicKey.algorithm.name, name); - expect(privateKey.algorithm.name, name); - expect(publicKey.algorithm.namedCurve, namedCurve); - expect(privateKey.algorithm.namedCurve, namedCurve); + const { publicKey, privateKey } = pair as CryptoKeyPair; + const pub = publicKey as CryptoKey; + const priv = privateKey as CryptoKey; + + expect(pub).is.not.undefined; + expect(priv).is.not.undefined; + expect(isCryptoKey(pub)); + expect(isCryptoKey(priv)); + expect(pub.type).to.equal('public'); + expect(priv.type).to.equal('private'); + expect(pub.keyExtractable).to.equal(true); + expect(priv.keyExtractable).to.equal(true); + expect(pub.keyUsages).to.deep.equal(publicUsages); + expect(priv.keyUsages).to.deep.equal(privateUsages); + expect(pub.algorithm.name, name); + expect(priv.algorithm.name, name); + expect(pub.algorithm.namedCurve, namedCurve); + expect(priv.algorithm.namedCurve, namedCurve); // Invalid parameters [1, true, {}, [], null].forEach(async (curve) => { diff --git a/example/src/testing/Tests/webcryptoTests/sign_verify.ts b/example/src/testing/Tests/webcryptoTests/sign_verify.ts new file mode 100644 index 00000000..f8435097 --- /dev/null +++ b/example/src/testing/Tests/webcryptoTests/sign_verify.ts @@ -0,0 +1,167 @@ +import type { CryptoKey, CryptoKeyPair } from '../../../../../src/keys'; +import crypto from 'react-native-quick-crypto'; +import { describe, it } from '../../MochaRNAdapter'; +import { expect } from 'chai'; + +// polyfill encoders +// @ts-expect-error +import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; +import RNFE from 'react-native-fast-encoder'; +polyfillGlobal('TextEncoder', () => RNFE); + +const { subtle } = crypto; + +describe('subtle - sign / verify', () => { + // // Test Sign/Verify RSASSA-PKCS1-v1_5 + // { + // async function test(data) { + // const ec = new TextEncoder(); + // const { publicKey, privateKey } = await subtle.generateKey({ + // name: 'RSASSA-PKCS1-v1_5', + // modulusLength: 1024, + // publicExponent: new Uint8Array([1, 0, 1]), + // hash: 'SHA-256' + // }, true, ['sign', 'verify']); + + // const signature = await subtle.sign({ + // name: 'RSASSA-PKCS1-v1_5' + // }, privateKey, ec.encode(data)); + + // assert(await subtle.verify({ + // name: 'RSASSA-PKCS1-v1_5' + // }, publicKey, signature, ec.encode(data))); + // } + + // test('hello world').then(common.mustCall()); + // } + + // // Test Sign/Verify RSA-PSS + // { + // async function test(data) { + // const ec = new TextEncoder(); + // const { publicKey, privateKey } = await subtle.generateKey({ + // name: 'RSA-PSS', + // modulusLength: 4096, + // publicExponent: new Uint8Array([1, 0, 1]), + // hash: 'SHA-256' + // }, true, ['sign', 'verify']); + + // const signature = await subtle.sign({ + // name: 'RSA-PSS', + // saltLength: 256, + // }, privateKey, ec.encode(data)); + + // assert(await subtle.verify({ + // name: 'RSA-PSS', + // saltLength: 256, + // }, publicKey, signature, ec.encode(data))); + // } + + // test('hello world').then(common.mustCall()); + // } + + // Test Sign/Verify ECDSA + // eslint-disable-next-line no-lone-blocks + { + async function test(data: string) { + const ec = new TextEncoder(); + const pair = await subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-384', + }, + true, + ['sign', 'verify'] + ); + const { publicKey, privateKey } = pair as CryptoKeyPair; + + const signature = await subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-384', + }, + privateKey as CryptoKey, + ec.encode(data) + ); + + expect( + await subtle.verify( + { + name: 'ECDSA', + hash: 'SHA-384', + }, + publicKey as CryptoKey, + signature, + ec.encode(data) + ) + ).to.equal(true); + } + + it('ECDSA', async () => { + await test('hello world'); + }); + } + + // // Test Sign/Verify HMAC + // { + // async function test(data) { + // const ec = new TextEncoder(); + + // const key = await subtle.generateKey({ + // name: 'HMAC', + // length: 256, + // hash: 'SHA-256' + // }, true, ['sign', 'verify']); + + // const signature = await subtle.sign({ + // name: 'HMAC', + // }, key, ec.encode(data)); + + // assert(await subtle.verify({ + // name: 'HMAC', + // }, key, signature, ec.encode(data))); + // } + + // test('hello world').then(common.mustCall()); + // } + + // // Test Sign/Verify Ed25519 + // { + // async function test(data) { + // const ec = new TextEncoder(); + // const { publicKey, privateKey } = await subtle.generateKey({ + // name: 'Ed25519', + // }, true, ['sign', 'verify']); + + // const signature = await subtle.sign({ + // name: 'Ed25519', + // }, privateKey, ec.encode(data)); + + // assert(await subtle.verify({ + // name: 'Ed25519', + // }, publicKey, signature, ec.encode(data))); + // } + + // test('hello world').then(common.mustCall()); + // } + + // // Test Sign/Verify Ed448 + // { + // async function test(data) { + // const ec = new TextEncoder(); + // const { publicKey, privateKey } = await subtle.generateKey({ + // name: 'Ed448', + // }, true, ['sign', 'verify']); + + // const signature = await subtle.sign({ + // name: 'Ed448', + // }, privateKey, ec.encode(data)); + + // assert(await subtle.verify({ + // name: 'Ed448', + // }, publicKey, signature, ec.encode(data))); + // } + + // test('hello world').then(common.mustCall()); + // } +}); diff --git a/example/yarn.lock b/example/yarn.lock index 2a9aeab0..46f0e556 100644 --- a/example/yarn.lock +++ b/example/yarn.lock @@ -1645,7 +1645,7 @@ resolved "https://registry.yarnpkg.com/@react-native/normalize-colors/-/normalize-colors-0.72.0.tgz#14294b7ed3c1d92176d2a00df48456e8d7d62212" integrity sha512-285lfdqSXaqKuBbbtP9qL2tDrfxdOFtIMvkKadtleRQkdOxx+uzGvFr82KHmc/sSiMtfXGp7JnFYWVh4sFl7Yw== -"@react-native/virtualized-lists@^0.72.8": +"@react-native/virtualized-lists@^0.72.4", "@react-native/virtualized-lists@^0.72.8": version "0.72.8" resolved "https://registry.yarnpkg.com/@react-native/virtualized-lists/-/virtualized-lists-0.72.8.tgz#a2c6a91ea0f1d40eb5a122fb063daedb92ed1dc3" integrity sha512-J3Q4Bkuo99k7mu+jPS9gSUSgq+lLRSI/+ahXNwV92XgJ/8UgOTxu2LPwhJnBk/sQKxq7E8WkZBnBiozukQMqrw== @@ -1785,6 +1785,14 @@ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563" integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng== +"@types/react-native@^0.72.7": + version "0.72.8" + resolved "https://registry.yarnpkg.com/@types/react-native/-/react-native-0.72.8.tgz#eb6238fab289f5f132f7ccf138bdfe6f21ed93e1" + integrity sha512-St6xA7+EoHN5mEYfdWnfYt0e8u6k2FR0P9s2arYgakQGFgU1f9FlPrIEcj0X24pLCF5c5i3WVuLCUdiCYHmOoA== + dependencies: + "@react-native/virtualized-lists" "^0.72.4" + "@types/react" "*" + "@types/react-test-renderer@^18.0.0": version "18.0.7" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-18.0.7.tgz#2cfe657adb3688cdf543995eceb2e062b5a68728" @@ -2100,6 +2108,11 @@ base64-js@^1.1.2, base64-js@^1.3.1, base64-js@^1.5.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +big-integer@^1.6.51: + version "1.6.52" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.52.tgz#60a887f3047614a8e1bffe5d7173490a97dc8c85" + integrity sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg== + binary-extensions@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" @@ -2919,6 +2932,11 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== +flatbuffers@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/flatbuffers/-/flatbuffers-2.0.6.tgz#3aa3a39d282af9a660b4a0cdd1bb7ad91874abfc" + integrity sha512-QTTZTXTbVfuOVQu2X6eLOw4vefUxnFJZxAKeN3rEPhjEzBtIbehimJLfVGHPM8iX0Na+9i76SBEg0skf0c0sCA== + flatted@^3.2.9: version "3.2.9" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" @@ -4729,6 +4747,14 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-native-fast-encoder@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/react-native-fast-encoder/-/react-native-fast-encoder-0.1.12.tgz#d7ac967b612b3188890480ceea19ca7a56450d56" + integrity sha512-y7rWdNkysf6VJtP/DPToQy5q14btm58hINlehL1IN4kH3cUaWcjnN8EYYd7rVXYlE811KpyP88XW2wi7xSH/0A== + dependencies: + big-integer "^1.6.51" + flatbuffers "2.0.6" + react-native-quick-base64@^2.0.5, react-native-quick-base64@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/react-native-quick-base64/-/react-native-quick-base64-2.1.2.tgz#062b09b165c1530095fe99b94544c948318dbe99" diff --git a/implementation-coverage.md b/implementation-coverage.md index 02a37f1f..ef617c5c 100644 --- a/implementation-coverage.md +++ b/implementation-coverage.md @@ -214,9 +214,9 @@ This document attempts to describe the implementation status of Crypto APIs/Inte * 🚧 `subtle.exportKey(format, key)` * 🚧 `subtle.generateKey(algorithm, extractable, keyUsages)` * 🚧 `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)` - * ❌ `subtle.sign(algorithm, key, data)` + * 🚧 `subtle.sign(algorithm, key, data)` * ❌ `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)` - * ❌ `subtle.verify(algorithm, key, signature, data)` + * 🚧 `subtle.verify(algorithm, key, signature, data)` * ❌ `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)` ## `subtle.decrypt` @@ -336,7 +336,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte | --------- | :----: | | `RSASSA-PKCS1-v1_5` | | | `RSA-PSS` | | -| `ECDSA` | | +| `ECDSA` | ✅ | | `Ed25519` | | | `Ed448` | | | `HMAC` | | @@ -375,7 +375,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte | --------- | :----: | | `RSASSA-PKCS1-v1_5` | | | `RSA-PSS` | | -| `ECDSA` | | +| `ECDSA` | ✅ | | `Ed25519` | | | `Ed448` | | | `HMAC` | | diff --git a/package.json b/package.json index f4bcff72..3daec72f 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,5 @@ "readable-stream": "^4.5.2", "string_decoder": "^1.3.0", "util": "^0.12.5" - }, - "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" + } } diff --git a/src/Cipher.ts b/src/Cipher.ts index 7309f1f3..57c78b36 100644 --- a/src/Cipher.ts +++ b/src/Cipher.ts @@ -34,6 +34,7 @@ import { Buffer } from '@craftzdog/react-native-buffer'; import { Buffer as SBuffer } from 'safe-buffer'; import { constants } from './constants'; import { + CryptoKey, parsePrivateKeyEncoding, parsePublicKeyEncoding, preparePrivateKey, @@ -456,21 +457,23 @@ export type GenerateKeyPairOptions = { mgf1Hash?: any; }; +export type KeyPairKey = Buffer | KeyObjectHandle | CryptoKey | undefined; + export type GenerateKeyPairReturn = [ error?: Error, - privateKey?: Buffer | KeyObjectHandle, - publicKey?: Buffer | KeyObjectHandle, + privateKey?: KeyPairKey, + publicKey?: KeyPairKey, ]; export type GenerateKeyPairCallback = ( error?: Error, - publicKey?: Buffer | KeyObjectHandle, - privateKey?: Buffer | KeyObjectHandle + publicKey?: KeyPairKey, + privateKey?: KeyPairKey ) => GenerateKeyPairReturn | void; export type KeyPair = { - publicKey?: Buffer | KeyObjectHandle; - privateKey?: Buffer | KeyObjectHandle; + publicKey?: KeyPairKey; + privateKey?: KeyPairKey; }; export type GenerateKeyPairPromiseReturn = [error?: Error, keypair?: KeyPair]; diff --git a/src/NativeQuickCrypto/sig.ts b/src/NativeQuickCrypto/sig.ts index 9fe77a27..9ce27436 100644 --- a/src/NativeQuickCrypto/sig.ts +++ b/src/NativeQuickCrypto/sig.ts @@ -1,4 +1,7 @@ // TODO Add real types to sign/verify, the problem is that because of encryption schemes + +import type { KeyObjectHandle } from './webcrypto'; + // they will have variable amount of parameters export type InternalSign = { init: (algorithm: string) => void; @@ -15,3 +18,27 @@ export type InternalVerify = { export type CreateSignMethod = () => InternalSign; export type CreateVerifyMethod = () => InternalVerify; + +export enum DSASigEnc { + kSigEncDER, + kSigEncP1363, +} + +export enum SignMode { + kSignJobModeSign, + kSignJobModeVerify, +} + +export type SignVerify = ( + mode: SignMode, + handle: KeyObjectHandle, + unused1: undefined, + unused2: undefined, + unused3: undefined, + data: ArrayBuffer, + digest: string | undefined, + salt_length: number | undefined, + padding: number | undefined, + dsa_encoding: DSASigEnc | undefined, + signature: ArrayBuffer | undefined +) => ArrayBuffer | boolean; diff --git a/src/NativeQuickCrypto/webcrypto.ts b/src/NativeQuickCrypto/webcrypto.ts index 110163b4..0940b2a6 100644 --- a/src/NativeQuickCrypto/webcrypto.ts +++ b/src/NativeQuickCrypto/webcrypto.ts @@ -7,6 +7,7 @@ import type { KWebCryptoKeyFormat, NamedCurve, } from '../keys'; +import type { SignVerify } from './sig'; type KeyDetail = { length?: number; @@ -43,4 +44,5 @@ type CreateKeyObjectHandle = () => KeyObjectHandle; export type webcrypto = { ecExportKey: ECExportKey; createKeyObjectHandle: CreateKeyObjectHandle; + signVerify: SignVerify; }; diff --git a/src/ec.ts b/src/ec.ts index f7be471a..57f348b2 100644 --- a/src/ec.ts +++ b/src/ec.ts @@ -1,5 +1,6 @@ import { generateKeyPairPromise, type GenerateKeyPairOptions } from './Cipher'; import { NativeQuickCrypto } from './NativeQuickCrypto/NativeQuickCrypto'; +import { DSASigEnc, SignMode } from './NativeQuickCrypto/sig'; import { bufferLikeToArrayBuffer, type BufferLike, @@ -10,6 +11,7 @@ import { hasAnyNotIn, ab2str, getUsagesUnion, + normalizeHashName, } from './Utils'; import { type ImportFormat, @@ -249,8 +251,8 @@ export function ecImportKey( case 'ECDSA': // Fall through case 'ECDH': - // if (keyObject.asymmetricKeyType !== 'ec') - // throw new Error('Invalid key type'); + if (keyObject.asymmetricKeyType !== 'ec') + throw new Error('Invalid key type'); break; } @@ -265,29 +267,38 @@ export function ecImportKey( return new CryptoKey(keyObject, { name, namedCurve }, keyUsages, extractable); } -// function ecdsaSignVerify(key, data, { name, hash }, signature) { -// const mode = signature === undefined ? kSignJobModeSign : kSignJobModeVerify; -// const type = mode === kSignJobModeSign ? 'private' : 'public'; - -// if (key.type !== type) -// throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); - -// const hashname = normalizeHashName(hash.name); - -// return jobPromise(() => new SignJob( -// kCryptoJobAsync, -// mode, -// key[kKeyObject][kHandle], -// undefined, -// undefined, -// undefined, -// data, -// hashname, -// undefined, // Salt length, not used with ECDSA -// undefined, // PSS Padding, not used with ECDSA -// kSigEncP1363, -// signature)); -// } +export const ecdsaSignVerify = ( + key: CryptoKey, + data: BufferLike, + { hash }: SubtleAlgorithm, + signature?: BufferLike +) => { + const mode: SignMode = + signature === undefined + ? SignMode.kSignJobModeSign + : SignMode.kSignJobModeVerify; + const type = mode === SignMode.kSignJobModeSign ? 'private' : 'public'; + + if (key.type !== type) + throw lazyDOMException(`Key must be a ${type} key`, 'InvalidAccessError'); + + const hashname = normalizeHashName(hash); + + return NativeQuickCrypto.webcrypto.signVerify( + mode, + key.keyObject.handle, + // three undefined args because C++ uses `GetPublicOrPrivateKeyFromJs` & friends + undefined, + undefined, + undefined, + bufferLikeToArrayBuffer(data), + hashname, + undefined, // salt length, not used with ECDSA + undefined, // pss padding, not used with ECDSA + DSASigEnc.kSigEncP1363, + bufferLikeToArrayBuffer(signature || new ArrayBuffer(0)) + ); +}; export const ecGenerateKey = async ( algorithm: SubtleAlgorithm, diff --git a/src/keys.ts b/src/keys.ts index 3de40153..7010fcba 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -5,6 +5,7 @@ import { } from './Utils'; import type { KeyObjectHandle } from './NativeQuickCrypto/webcrypto'; import { NativeQuickCrypto } from './NativeQuickCrypto/NativeQuickCrypto'; +import type { KeyPairKey } from './Cipher'; export const kNamedCurveAliases = { 'P-256': 'prime256v1', @@ -176,8 +177,8 @@ const encodingNames = { }; export type CryptoKeyPair = { - publicKey: any; - privateKey: any; + publicKey: KeyPairKey; + privateKey: KeyPairKey; }; function option(name: string, objName: string | undefined) { @@ -602,12 +603,17 @@ class AsymmetricKeyObject extends KeyObject { super(type, handle); } + private _asymmetricKeyType?: AsymmetricKeyType; + get asymmetricKeyType(): AsymmetricKeyType { - return this.asymmetricKeyType || this.handle.getAsymmetricKeyType(); + if (!this._asymmetricKeyType) { + this._asymmetricKeyType = this.handle.getAsymmetricKeyType(); + } + return this._asymmetricKeyType; } // get asymmetricKeyDetails() { - // switch (this.asymmetricKeyType) { + // switch (this._asymmetricKeyType) { // case 'rsa': // case 'rsa-pss': // case 'dsa': diff --git a/src/subtle.ts b/src/subtle.ts index 40c254cb..547acf4e 100644 --- a/src/subtle.ts +++ b/src/subtle.ts @@ -17,8 +17,9 @@ import { lazyDOMException, normalizeHashName, HashContext, + type Operation, } from './Utils'; -import { ecImportKey, ecExportKey, ecGenerateKey } from './ec'; +import { ecImportKey, ecExportKey, ecGenerateKey, ecdsaSignVerify } from './ec'; import { pbkdf2DeriveBits } from './pbkdf2'; import { asyncDigest } from './Hash'; import { aesImportKey, getAlgorithmName } from './aes'; @@ -216,12 +217,72 @@ const importGenericSecretKey = async ( // }; const checkCryptoKeyPairUsages = (pair: CryptoKeyPair) => { - if (pair.privateKey.usages.length === 0) { + if ( + !(pair.privateKey instanceof Buffer) && + pair.privateKey && + pair.privateKey.hasOwnProperty('keyUsages') + ) { + const priv = pair.privateKey as CryptoKey; + if (priv.usages.length > 0) { + return; + } + } + console.log(pair.privateKey); + throw lazyDOMException( + 'Usages cannot be empty when creating a key.', + 'SyntaxError' + ); +}; + +const signVerify = ( + algorithm: SubtleAlgorithm, + key: CryptoKey, + data: BufferLike, + signature?: BufferLike +): ArrayBuffer | boolean => { + const usage: Operation = signature === undefined ? 'sign' : 'verify'; + algorithm = normalizeAlgorithm(algorithm, usage); + + if (!key.usages.includes(usage) || algorithm.name !== key.algorithm.name) { throw lazyDOMException( - 'Usages cannot be empty when creating a key.', - 'SyntaxError' + `Unable to use this key to ${usage}`, + 'InvalidAccessError' ); } + + switch (algorithm.name) { + // case 'RSA-PSS': + // // Fall through + // case 'RSASSA-PKCS1-v1_5': + // return require('internal/crypto/rsa').rsaSignVerify( + // key, + // data, + // algorithm, + // signature + // ); + case 'ECDSA': + return ecdsaSignVerify(key, data, algorithm, signature); + // case 'Ed25519': + // // Fall through + // case 'Ed448': + // return require('internal/crypto/cfrg').eddsaSignVerify( + // key, + // data, + // algorithm, + // signature + // ); + // case 'HMAC': + // return require('internal/crypto/mac').hmacSignVerify( + // key, + // data, + // algorithm, + // signature + // ); + } + throw lazyDOMException( + `Unrecognized algorithm name '${algorithm}' for '${usage}'`, + 'NotSupportedError' + ); }; class Subtle { @@ -438,6 +499,23 @@ class Subtle { return result; } + + async sign( + algorithm: SubtleAlgorithm, + key: CryptoKey, + data: BufferLike + ): Promise { + return signVerify(algorithm, key, data) as ArrayBuffer; + } + + async verify( + algorithm: SubtleAlgorithm, + key: CryptoKey, + signature: BufferLike, + data: BufferLike + ): Promise { + return signVerify(algorithm, key, data, signature) as ArrayBuffer; + } } export const subtle = new Subtle();