diff --git a/README.md b/README.md index 279d2499..b1f09120 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,31 @@ const hashed = Crypto.createHash('sha256') .digest('hex'); ``` +## Android build errors + +If you get an error similar to this: + +``` +Execution failed for task ':app:mergeDebugNativeLibs'. +> A failure occurred while executing com.android.build.gradle.internal.tasks.MergeNativeLibsTask$MergeNativeLibsTaskWorkAction + > 2 files found with path 'lib/arm64-v8a/libcrypto.so' from inputs: + - /Users/osp/Developer/mac_test/node_modules/react-native-quick-crypto/android/build/intermediates/library_jni/debug/jni/arm64-v8a/libcrypto.so + - /Users/osp/.gradle/caches/transforms-3/e13f88164840fe641a466d05cd8edac7/transformed/jetified-flipper-0.182.0/jni/arm64-v8a/libcrypto.so +``` + +It means you have a transitive dependency where two libraries depend on OpenSSL and are generating a `libcrypto.so` file. You can get around this issue by adding the following in your `app/build.gradle`: + +```groovy +packagingOptions { + // Should prevent clashes with other libraries that use OpenSSL + pickFirst '**/libcrypto.so' +} +``` + +> This caused by flipper which also depends on OpenSSL + +This just tells Gradle to grab whatever OpenSSL version it finds first and link against that, but as you can imagine this is not correct if the packages depend on different OpenSSL versions (quick-crypto depends on `com.android.ndk.thirdparty:openssl:1.1.1q-beta-1`). You should make sure all the OpenSSL versions match and you have no conflicts or errors. + --- ## Sponsors diff --git a/android/CMakeLists.txt b/android/CMakeLists.txt index 8e9f0a88..a680fbc9 100644 --- a/android/CMakeLists.txt +++ b/android/CMakeLists.txt @@ -50,6 +50,8 @@ add_library( "../cpp/Sig/MGLSignInstaller.cpp" "../cpp/Sig/MGLVerifyInstaller.cpp" "../cpp/Sig/MGLSignHostObjects.cpp" + "../cpp/webcrypto/MGLWebCrypto.cpp" + "../cpp/webcrypto/crypto_ec.cpp" ) set_target_properties( diff --git a/android/build.gradle b/android/build.gradle index de8e67d8..8b71eca3 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -91,11 +91,9 @@ android { "**/libturbomodulejsijni.so", "**/MANIFEST.MF", ] - // Should prevent clashes with other libraries that use OpenSSL - pickFirst '**/x86/libcrypto.so' - pickFirst '**/x86_64/libcrypto.so' - pickFirst '**/armeabi-v7a/libcrypto.so' - pickFirst '**/arm64-v8a/libcrypto.so' + // Setting pickFirst on this level does nothing + // pickFirst should be added on the app/build.gradle + // pickFirst '**/libcrypto.so' } buildTypes { diff --git a/cpp/JSIUtils/MGLJSIMacros.h b/cpp/JSIUtils/MGLJSIMacros.h index 310a9df3..83a80cab 100644 --- a/cpp/JSIUtils/MGLJSIMacros.h +++ b/cpp/JSIUtils/MGLJSIMacros.h @@ -32,6 +32,16 @@ inline void Assert(const AssertionInfo &info) { Abort(); } +#define HOSTFN(name, basecount) \ + jsi::Function::createFromHostFunction( \ + rt, \ + jsi::PropNameID::forAscii(rt, name), \ + basecount, \ + [=](jsi::Runtime &rt, \ + const jsi::Value &thisValue, \ + const jsi::Value *args, \ + size_t count) -> jsi::Value + #define HOST_LAMBDA(name, body) HOST_LAMBDA_CAP(name, [=], body) #define HOST_LAMBDA_CAP(name, capture, body) \ diff --git a/cpp/MGLKeys.cpp b/cpp/MGLKeys.cpp index 57de4906..3d175f17 100644 --- a/cpp/MGLKeys.cpp +++ b/cpp/MGLKeys.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -20,11 +21,13 @@ #include "JSIUtils/MGLJSIUtils.h" #include "JSIUtils/MGLTypedArray.h" #include "Utils/MGLUtils.h" +#include "webcrypto/crypto_ec.h" #else #include "MGLJSIMacros.h" #include "MGLJSIUtils.h" #include "MGLTypedArray.h" #include "MGLUtils.h" +#include "crypto_ec.h" #endif namespace margelo { @@ -818,54 +821,36 @@ ManagedEVPPKey ManagedEVPPKey::GetParsedKey(jsi::Runtime& runtime, // symmetric_key_len_(symmetric_key_.size()), // asymmetric_key_() {} // -// KeyObjectData::KeyObjectData( -// KeyType type, -// const ManagedEVPPKey& pkey) -//: key_type_(type), +KeyObjectData::KeyObjectData(KeyType type, + const ManagedEVPPKey& pkey) +: key_type_(type), // symmetric_key_(), // symmetric_key_len_(0), -// asymmetric_key_{pkey} {} -// -// void KeyObjectData::MemoryInfo(MemoryTracker* tracker) const { -// switch (GetKeyType()) { -// case kKeyTypeSecret: -// tracker->TrackFieldWithSize("symmetric_key", symmetric_key_.size()); -// break; -// case kKeyTypePrivate: -// // Fall through -// case kKeyTypePublic: -// tracker->TrackFieldWithSize("key", asymmetric_key_); -// break; -// default: -// UNREACHABLE(); -// } -// } -// + asymmetric_key_{pkey} {} + // std::shared_ptr KeyObjectData::CreateSecret(ByteSource key) // { // CHECK(key); // return std::shared_ptr(new KeyObjectData(std::move(key))); // } -// -// std::shared_ptr KeyObjectData::CreateAsymmetric( -// KeyType -// key_type, -// const -// ManagedEVPPKey& -// pkey) { -// CHECK(pkey); -// return std::shared_ptr(new KeyObjectData(key_type, pkey)); -// } -// -// KeyType KeyObjectData::GetKeyType() const { -// return key_type_; -// } -// -// ManagedEVPPKey KeyObjectData::GetAsymmetricKey() const { + +std::shared_ptr KeyObjectData::CreateAsymmetric( + KeyType key_type, + const ManagedEVPPKey& pkey +) { + CHECK(pkey); + return std::shared_ptr(new KeyObjectData(key_type, pkey)); +} + +KeyType KeyObjectData::GetKeyType() const { + return key_type_; +} + +ManagedEVPPKey KeyObjectData::GetAsymmetricKey() const { // CHECK_NE(key_type_, kKeyTypeSecret); -// return asymmetric_key_; -// } -// + return asymmetric_key_; +} + // const char* KeyObjectData::GetSymmetricKey() const { // CHECK_EQ(key_type_, kKeyTypeSecret); // return symmetric_key_.data(); @@ -1033,6 +1018,51 @@ ManagedEVPPKey ManagedEVPPKey::GetParsedKey(jsi::Runtime& runtime, // // args.GetReturnValue().Set(key->data_->GetKeyType()); //} + +jsi::Value KeyObjectHandle::get( + jsi::Runtime &rt, + const jsi::PropNameID &propNameID) { + auto name = propNameID.utf8(rt); + + if (name == "initECRaw") { + return HOSTFN("initECRaw", 2) { + CHECK(args[0].isString()); + std::string curveName = args[0].asString(rt).utf8(rt); + int id = OBJ_txt2nid(curveName.c_str()); + ECKeyPointer eckey(EC_KEY_new_by_curve_name(id)); + if (!eckey) { + return false; + } + // TODO(osp) add validation + auto buf = args[1].asObject(rt).getArrayBuffer(rt); + + const EC_GROUP* group = EC_KEY_get0_group(eckey.get()); + ECPointPointer pub(ECDH::BufferToPoint(rt, group, buf)); + + if (!pub || + !eckey || + !EC_KEY_set_public_key(eckey.get(), pub.get())) { + return false; + } + + EVPKeyPointer pkey(EVP_PKEY_new()); + if (!EVP_PKEY_assign_EC_KEY(pkey.get(), eckey.get())) { + return false; + } + + eckey.release(); // Release ownership of the key + + this->data_ = + KeyObjectData::CreateAsymmetric( + kKeyTypePublic, + ManagedEVPPKey(std::move(pkey))); + + return true; + }); + } + return {}; +} + // // void KeyObjectHandle::InitECRaw(const FunctionCallbackInfo& args) { // Environment* env = Environment::GetCurrent(args); @@ -1456,4 +1486,5 @@ ManagedEVPPKey ManagedEVPPKey::GetParsedKey(jsi::Runtime& runtime, // void RegisterExternalReferences(ExternalReferenceRegistry * registry) { // KeyObjectHandle::RegisterExternalReferences(registry); // } + } // namespace margelo diff --git a/cpp/MGLKeys.h b/cpp/MGLKeys.h index db52cc65..5f2c1c36 100644 --- a/cpp/MGLKeys.h +++ b/cpp/MGLKeys.h @@ -19,8 +19,11 @@ #include "Utils/MGLUtils.h" #else #include "MGLUtils.h" +#include "JSIUtils/MGLSmartHostObject.h" #endif +// This file should roughly match https://github.com/nodejs/node/blob/main/src/crypto/crypto_keys.cc + namespace margelo { namespace jsi = facebook::jsi; @@ -118,6 +121,46 @@ class ManagedEVPPKey { EVPKeyPointer pkey_; }; +// Analogous to the KeyObjectData class on node +// https://github.com/nodejs/node/blob/main/src/crypto/crypto_keys.h#L132 +class KeyObjectData { + public: +// static std::shared_ptr CreateSecret(ByteSource key); + + static std::shared_ptr CreateAsymmetric( + KeyType type, + const ManagedEVPPKey& pkey); + + KeyType GetKeyType() const; + + // These functions allow unprotected access to the raw key material and should + // only be used to implement cryptographic operations requiring the key. + ManagedEVPPKey GetAsymmetricKey() const; +// const char* GetSymmetricKey() const; +// size_t GetSymmetricKeySize() const; + + private: +// explicit KeyObjectData(ByteSource symmetric_key); + + KeyObjectData( + KeyType type, + const ManagedEVPPKey& pkey); + + const KeyType key_type_; +// const ByteSource symmetric_key_; + const ManagedEVPPKey asymmetric_key_; +}; + +// Analoguous to the KeyObjectHandle class in node +// https://github.com/nodejs/node/blob/main/src/crypto/crypto_keys.h#L164 +class JSI_EXPORT KeyObjectHandle: public jsi::HostObject { + public: + KeyObjectHandle() {} + jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &propNameID); + // TODO(osp) this should be protected + std::shared_ptr data_; +}; + } // namespace margelo #endif /* MGLCipherKeys_h */ diff --git a/cpp/MGLQuickCryptoHostObject.cpp b/cpp/MGLQuickCryptoHostObject.cpp index ac4348a4..231ec0be 100644 --- a/cpp/MGLQuickCryptoHostObject.cpp +++ b/cpp/MGLQuickCryptoHostObject.cpp @@ -21,6 +21,7 @@ #include "Sig/MGLSignInstaller.h" #include "Sig/MGLVerifyInstaller.h" #include "fastpbkdf2/MGLPbkdf2HostObject.h" +#include "webcrypto/MGLWebCrypto.h" #else #include "MGLCreateCipherInstaller.h" #include "MGLCreateDecipherInstaller.h" @@ -34,6 +35,7 @@ #include "MGLRandomHostObject.h" #include "MGLSignInstaller.h" #include "MGLVerifyInstaller.h" +#include "MGLWebCrypto.h" #endif namespace margelo { @@ -105,11 +107,12 @@ MGLQuickCryptoHostObject::MGLQuickCryptoHostObject( return jsi::Object::createFromHostObject(runtime, hostObject); })); - // createSign - this->fields.push_back(getSignFieldDefinition(jsCallInvoker, workerQueue)); - - // createVerify - this->fields.push_back(getVerifyFieldDefinition(jsCallInvoker, workerQueue)); + // subtle API created from a simple jsi::Object + // because this FieldDefinition is only good for returning + // objects and too convoluted + this->fields.push_back(JSI_VALUE("webcrypto", { + return createWebCryptoObject(runtime); + })); } } // namespace margelo diff --git a/cpp/Random/MGLRandomHostObject.cpp b/cpp/Random/MGLRandomHostObject.cpp index 718f7eb6..7231938f 100644 --- a/cpp/Random/MGLRandomHostObject.cpp +++ b/cpp/Random/MGLRandomHostObject.cpp @@ -41,6 +41,11 @@ MGLRandomHostObject::MGLRandomHostObject( throw std::runtime_error("First argument it not an array buffer"); } + if (!arguments[0].isObject() + || !arguments[0].asObject(runtime).isArrayBuffer(runtime)) { + throw std::runtime_error("First argument it not an array buffer"); + } + auto result = arguments[0].asObject(runtime).getArrayBuffer(runtime); auto resultSize = result.size(runtime); auto *resultData = result.data(runtime); diff --git a/cpp/Utils/MGLUtils.cpp b/cpp/Utils/MGLUtils.cpp index c3011a26..0ee0bd12 100644 --- a/cpp/Utils/MGLUtils.cpp +++ b/cpp/Utils/MGLUtils.cpp @@ -14,6 +14,23 @@ namespace margelo { namespace jsi = facebook::jsi; +jsi::Object ByteSourceToArrayBuffer(jsi::Runtime &rt, + ByteSource &source) { + jsi::Function array_buffer_ctor = rt.global() + .getPropertyAsFunction(rt, "ArrayBuffer"); + jsi::Object o = array_buffer_ctor.callAsConstructor( + rt, + (int)source.size()) + .getObject(rt); + + jsi::ArrayBuffer buf = o.getArrayBuffer(rt); + // You cannot share raw memory between native and JS + // always copy the data + // see https://github.com/facebook/hermes/pull/419 and https://github.com/facebook/hermes/issues/564. + memcpy(buf.data(rt), source.data(), source.size()); + return o; +} + ByteSource ArrayBufferToByteSource(jsi::Runtime& runtime, const jsi::ArrayBuffer& buffer) { if (buffer.size(runtime) == 0) return ByteSource(); @@ -96,14 +113,14 @@ ByteSource& ByteSource::operator=(ByteSource&& other) noexcept { // return Buffer::New(env, ab, 0, ab->ByteLength()); // } -// ByteSource ByteSource::FromBIO(const BIOPointer& bio) { -//// CHECK(bio); -// BUF_MEM* bptr; -// BIO_get_mem_ptr(bio.get(), &bptr); -// ByteSource::Builder out(bptr->length); -// memcpy(out.data(), bptr->data, bptr->length); -// return std::move(out).release(); -//} +ByteSource ByteSource::FromBIO(const BIOPointer& bio) { +// CHECK(bio); + BUF_MEM* bptr; + BIO_get_mem_ptr(bio.get(), &bptr); + ByteSource::Builder out(bptr->length); + memcpy(out.data(), bptr->data, bptr->length); + return std::move(out).release(); +} // ByteSource ByteSource::FromEncodedString(Environment* env, // Local key, diff --git a/cpp/Utils/MGLUtils.h b/cpp/Utils/MGLUtils.h index b1a8519f..fbf4be3d 100644 --- a/cpp/Utils/MGLUtils.h +++ b/cpp/Utils/MGLUtils.h @@ -14,7 +14,6 @@ #endif // !OPENSSL_NO_ENGINE #include - #include #include #include @@ -26,15 +25,15 @@ namespace margelo { namespace jsi = facebook::jsi; struct StringOrBuffer { - bool isString; - std::string stringValue; - std::vector vectorValue; + bool isString; + std::string stringValue; + std::vector vectorValue; }; template struct FunctionDeleter { - void operator()(T* pointer) const { function(pointer); } - typedef std::unique_ptr Pointer; + void operator()(T* pointer) const { function(pointer); } + typedef std::unique_ptr Pointer; }; template @@ -48,164 +47,166 @@ using BignumPointer = DeleteFnPtr; using RSAPointer = DeleteFnPtr; using EVPMDPointer = DeleteFnPtr; using ECDSASigPointer = DeleteFnPtr; +using ECKeyPointer = DeleteFnPtr; +using ECPointPointer = DeleteFnPtr; template class NonCopyableMaybe { public: - NonCopyableMaybe() : empty_(true) {} - explicit NonCopyableMaybe(T&& value) - : empty_(false), value_(std::move(value)) {} + NonCopyableMaybe() : empty_(true) {} + explicit NonCopyableMaybe(T&& value) + : empty_(false), value_(std::move(value)) {} - bool IsEmpty() const { return empty_; } + bool IsEmpty() const { return empty_; } - const T* get() const { return empty_ ? nullptr : &value_; } + const T* get() const { return empty_ ? nullptr : &value_; } - const T* operator->() const { - // CHECK(!empty_); - return &value_; - } + const T* operator->() const { + // CHECK(!empty_); + return &value_; + } - T&& Release() { - // CHECK_EQ(empty_, false); - empty_ = true; - return std::move(value_); - } + T&& Release() { + // CHECK_EQ(empty_, false); + empty_ = true; + return std::move(value_); + } private: - bool empty_; - T value_; + bool empty_; + T value_; }; template inline T MultiplyWithOverflowCheck(T a, T b) { - auto ret = a * b; - // if (a != 0) - // CHECK_EQ(b, ret / a); + auto ret = a * b; + // if (a != 0) + // CHECK_EQ(b, ret / a); - return ret; + return ret; } template T* MallocOpenSSL(size_t count) { - void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); - // CHECK_IMPLIES(mem == nullptr, count == 0); - return static_cast(mem); + void* mem = OPENSSL_malloc(MultiplyWithOverflowCheck(count, sizeof(T))); + // CHECK_IMPLIES(mem == nullptr, count == 0); + return static_cast(mem); } // A helper class representing a read-only byte array. When deallocated, its // contents are zeroed. class ByteSource { public: - class Builder { - public: - // Allocates memory using OpenSSL's memory allocator. - explicit Builder(size_t size) + class Builder { + public: + // Allocates memory using OpenSSL's memory allocator. + explicit Builder(size_t size) : data_(MallocOpenSSL(size)), size_(size) {} - Builder(Builder&& other) = delete; - Builder& operator=(Builder&& other) = delete; - Builder(const Builder&) = delete; - Builder& operator=(const Builder&) = delete; - - ~Builder() { OPENSSL_clear_free(data_, size_); } + Builder(Builder&& other) = delete; + Builder& operator=(Builder&& other) = delete; + Builder(const Builder&) = delete; + Builder& operator=(const Builder&) = delete; - // Returns the underlying non-const pointer. - template - T* data() { - return reinterpret_cast(data_); - } + ~Builder() { OPENSSL_clear_free(data_, size_); } - // Returns the (allocated) size in bytes. - size_t size() const { return size_; } + // Returns the underlying non-const pointer. + template + T* data() { + return reinterpret_cast(data_); + } - // Finalizes the Builder and returns a read-only view that is optionally - // truncated. - ByteSource release(std::optional resize = std::nullopt) && { - if (resize) { - // CHECK_LE(*resize, size_); - if (*resize == 0) { - OPENSSL_clear_free(data_, size_); - data_ = nullptr; + // Returns the (allocated) size in bytes. + size_t size() const { return size_; } + + // Finalizes the Builder and returns a read-only view that is optionally + // truncated. + ByteSource release(std::optional resize = std::nullopt) && { + if (resize) { + // CHECK_LE(*resize, size_); + if (*resize == 0) { + OPENSSL_clear_free(data_, size_); + data_ = nullptr; + } + size_ = *resize; + } + ByteSource out = ByteSource::Allocated(data_, size_); + data_ = nullptr; + size_ = 0; + return out; } - size_ = *resize; - } - ByteSource out = ByteSource::Allocated(data_, size_); - data_ = nullptr; - size_ = 0; - return out; - } - private: - void* data_; - size_t size_; - }; + private: + void* data_; + size_t size_; + }; - ByteSource() = default; - ByteSource(ByteSource&& other) noexcept; - ~ByteSource(); + ByteSource() = default; + ByteSource(ByteSource&& other) noexcept; + ~ByteSource(); - ByteSource& operator=(ByteSource&& other) noexcept; + ByteSource& operator=(ByteSource&& other) noexcept; - ByteSource(const ByteSource&) = delete; - ByteSource& operator=(const ByteSource&) = delete; + ByteSource(const ByteSource&) = delete; + ByteSource& operator=(const ByteSource&) = delete; - template - const T* data() const { - return reinterpret_cast(data_); - } + template + const T* data() const { + return reinterpret_cast(data_); + } + + size_t size() const { return size_; } - size_t size() const { return size_; } + operator bool() const { return data_ != nullptr; } - operator bool() const { return data_ != nullptr; } + // BignumPointer ToBN() const { + // return BignumPointer(BN_bin2bn(data(), size(), nullptr)); + // } - // BignumPointer ToBN() const { - // return BignumPointer(BN_bin2bn(data(), size(), nullptr)); - // } + // Creates a v8::BackingStore that takes over responsibility for + // any allocated data. The ByteSource will be reset with size = 0 + // after being called. + // std::unique_ptr ReleaseToBackingStore(); + // + // v8::Local ToArrayBuffer(Environment* env); + // + // v8::MaybeLocal ToBuffer(Environment* env); - // Creates a v8::BackingStore that takes over responsibility for - // any allocated data. The ByteSource will be reset with size = 0 - // after being called. - // std::unique_ptr ReleaseToBackingStore(); - // - // v8::Local ToArrayBuffer(Environment* env); - // - // v8::MaybeLocal ToBuffer(Environment* env); + static ByteSource Allocated(void* data, size_t size); + static ByteSource Foreign(const void* data, size_t size); - static ByteSource Allocated(void* data, size_t size); - static ByteSource Foreign(const void* data, size_t size); + // static ByteSource FromEncodedString(Environment* env, + // v8::Local value, + // enum encoding enc = BASE64); + // + static ByteSource FromStringOrBuffer(jsi::Runtime& runtime, + const jsi::Value& value); - // static ByteSource FromEncodedString(Environment* env, - // v8::Local value, - // enum encoding enc = BASE64); - // - static ByteSource FromStringOrBuffer(jsi::Runtime& runtime, - const jsi::Value& value); + static ByteSource FromString(std::string str, bool ntc = false); - static ByteSource FromString(std::string str, bool ntc = false); + static ByteSource FromBuffer(jsi::Runtime& runtime, + const jsi::ArrayBuffer& buffer, + bool ntc = false); - static ByteSource FromBuffer(jsi::Runtime& runtime, - const jsi::ArrayBuffer& buffer, - bool ntc = false); + static ByteSource FromBIO(const BIOPointer& bio); - // static ByteSource FromBIO(const BIOPointer& bio); - // - // static ByteSource NullTerminatedCopy(Environment* env, - // v8::Local value); - // - // static ByteSource FromSymmetricKeyObjectHandle(v8::Local - // handle); + // static ByteSource NullTerminatedCopy(Environment* env, + // v8::Local value); + // + // static ByteSource FromSymmetricKeyObjectHandle(v8::Local + // handle); - // static ByteSource FromSecretKeyBytes( - // Environment* env, - // v8::Local value); + // static ByteSource FromSecretKeyBytes( + // Environment* env, + // v8::Local value); private: - const void* data_ = nullptr; - void* allocated_data_ = nullptr; - size_t size_ = 0; + const void* data_ = nullptr; + void* allocated_data_ = nullptr; + size_t size_ = 0; - ByteSource(const void* data, void* allocated_data, size_t size) - : data_(data), allocated_data_(allocated_data), size_(size) {} + ByteSource(const void* data, void* allocated_data, size_t size) + : data_(data), allocated_data_(allocated_data), size_(size) {} }; ByteSource ArrayBufferToByteSource(jsi::Runtime& runtime, @@ -214,39 +215,42 @@ ByteSource ArrayBufferToByteSource(jsi::Runtime& runtime, ByteSource ArrayBufferToNTCByteSource(jsi::Runtime& runtime, const jsi::ArrayBuffer& buffer); +jsi::Object ByteSourceToArrayBuffer(jsi::Runtime &runtime, + ByteSource &source); + // Originally part of the ArrayBufferContentOrView class inline ByteSource ToNullTerminatedByteSource(jsi::Runtime& runtime, jsi::ArrayBuffer& buffer) { - if (buffer.size(runtime) == 0) return ByteSource(); - char* buf = MallocOpenSSL(buffer.size(runtime) + 1); - // CHECK_NOT_NULL(buf); - buf[buffer.size(runtime)] = 0; - memcpy(buf, buffer.data(runtime), buffer.size(runtime)); - return ByteSource::Allocated(buf, buffer.size(runtime)); + if (buffer.size(runtime) == 0) return ByteSource(); + char* buf = MallocOpenSSL(buffer.size(runtime) + 1); + // CHECK_NOT_NULL(buf); + buf[buffer.size(runtime)] = 0; + memcpy(buf, buffer.data(runtime), buffer.size(runtime)); + return ByteSource::Allocated(buf, buffer.size(runtime)); } inline int PasswordCallback(char* buf, int size, int rwflag, void* u) { - const ByteSource* passphrase = *static_cast(u); - if (passphrase != nullptr) { - size_t buflen = static_cast(size); - size_t len = passphrase->size(); - if (buflen < len) return -1; - memcpy(buf, passphrase->data(), len); - return len; - } - - return -1; + const ByteSource* passphrase = *static_cast(u); + if (passphrase != nullptr) { + size_t buflen = static_cast(size); + size_t len = passphrase->size(); + if (buflen < len) return -1; + memcpy(buf, passphrase->data(), len); + return len; + } + + return -1; } inline void CheckEntropy() { - for (;;) { - int status = RAND_status(); - // CHECK_GE(status, 0); // Cannot fail. - if (status != 0) break; - - // Give up, RAND_poll() not supported. - if (RAND_poll() == 0) break; - } + for (;;) { + int status = RAND_status(); + // CHECK_GE(status, 0); // Cannot fail. + if (status != 0) break; + + // Give up, RAND_poll() not supported. + if (RAND_poll() == 0) break; + } } } // namespace margelo diff --git a/cpp/webcrypto/MGLWebCrypto.cpp b/cpp/webcrypto/MGLWebCrypto.cpp new file mode 100644 index 00000000..450d749f --- /dev/null +++ b/cpp/webcrypto/MGLWebCrypto.cpp @@ -0,0 +1,54 @@ +// +// MGLWebCrypto.cpp +// react-native-quick-crypto +// +// Created by Oscar Franco on 1/12/23. +// + +#include "MGLWebCrypto.h" + +#include +#include +#include "MGLKeys.h" +#ifdef ANDROID +#include "JSIUtils/MGLJSIMacros.h" +#include "webcrypto/crypto_ec.h" +#else +#include "MGLJSIMacros.h" +#include "crypto_ec.h" +#endif + +namespace margelo { +namespace jsi = facebook::jsi; +namespace react = facebook::react; + +jsi::Value createWebCryptoObject(jsi::Runtime &rt) { + auto obj = jsi::Object(rt); + + auto createKeyObjectHandle = HOSTFN("createKeyObjectHandle", 0) { + auto keyObjectHandleHostObject = + std::make_shared(); + return jsi::Object::createFromHostObject(rt, keyObjectHandleHostObject); + }); + + auto ecExportKey = HOSTFN("ecExportKey", 2) { + ByteSource out; + std::shared_ptr handle = + std::static_pointer_cast( + args[1].asObject(rt).getHostObject(rt)); + std::shared_ptr key_data = handle->data_; + ECDH::doExport(rt, static_cast(args[0].asNumber()), + key_data, &out); + auto buffer = ByteSourceToArrayBuffer(rt, out); + return buffer; + }); + + obj.setProperty(rt, + "createKeyObjectHandle", + std::move(createKeyObjectHandle)); + obj.setProperty(rt, "ecExportKey", std::move(ecExportKey)); + return obj; +}; + +} // namespace margelo + diff --git a/cpp/webcrypto/MGLWebCrypto.h b/cpp/webcrypto/MGLWebCrypto.h new file mode 100644 index 00000000..b7b6da69 --- /dev/null +++ b/cpp/webcrypto/MGLWebCrypto.h @@ -0,0 +1,34 @@ +// +// MGLWebCrypto.hpp +// react-native-quick-crypto +// +// Created by Oscar Franco on 1/12/23. +// + +#ifndef MGLWebCryptoHostObject_h +#define MGLWebCryptoHostObject_h + +#include +#include + +#ifdef ANDROID +#include "JSIUtils/MGLSmartHostObject.h" +#else +#include "MGLSmartHostObject.h" +#endif + +namespace margelo { +namespace jsi = facebook::jsi; + +enum WebCryptoKeyFormat { + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, + kWebCryptoKeyFormatJWK +}; + +jsi::Value createWebCryptoObject(jsi::Runtime &rt); + +} // namespace margelo + +#endif /* MGLWebCrypto_hpp */ diff --git a/cpp/webcrypto/crypto_ec.cpp b/cpp/webcrypto/crypto_ec.cpp new file mode 100644 index 00000000..fa3b3e6d --- /dev/null +++ b/cpp/webcrypto/crypto_ec.cpp @@ -0,0 +1,129 @@ +// +// crypto_ec.cpp +// BEMCheckBox +// +// Created by Oscar Franco on 30/11/23. +// + +#include "crypto_ec.h" +#include + +namespace margelo { +namespace jsi = facebook::jsi; + +ECPointPointer ECDH::BufferToPoint(jsi::Runtime &rt, + const EC_GROUP* group, + jsi::ArrayBuffer &buf) { + int r; + + ECPointPointer pub(EC_POINT_new(group)); + if (!pub) { + throw std::runtime_error( + "Failed to allocate EC_POINT for a public key"); + return pub; + } + + // TODO(osp) re-insert this check + // if (UNLIKELY(!input.CheckSizeInt32())) { + // THROW_ERR_OUT_OF_RANGE(env, "buffer is too big"); + // return ECPointPointer(); + // } + r = EC_POINT_oct2point( + group, + pub.get(), + buf.data(rt), + buf.size(rt), + nullptr); + + if (!r) + return ECPointPointer(); + + return pub; +} + +void PKEY_SPKI_Export( + KeyObjectData* key_data, + ByteSource* out) { + CHECK_EQ(key_data->GetKeyType(), kKeyTypePublic); + ManagedEVPPKey m_pkey = key_data->GetAsymmetricKey(); + // Mutex::ScopedLock lock(*m_pkey.mutex()); + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + if (!i2d_PUBKEY_bio(bio.get(), m_pkey.get())) + throw std::runtime_error("Failed to export key"); + + *out = ByteSource::FromBIO(bio); +} + +void ECDH::doExport(jsi::Runtime &rt, + WebCryptoKeyFormat format, + std::shared_ptr key_data, + ByteSource* out) { + // CHECK_NE(key_data->GetKeyType(), kKeyTypeSecret); + + switch (format) { + // case kWebCryptoKeyFormatRaw: + // return EC_Raw_Export(key_data.get(), params, out); + // case kWebCryptoKeyFormatPKCS8: + // if (key_data->GetKeyType() != kKeyTypePrivate) + // return WebCryptoKeyExportStatus::INVALID_KEY_TYPE; + // return PKEY_PKCS8_Export(key_data.get(), out); + case kWebCryptoKeyFormatSPKI: { + if (key_data->GetKeyType() != kKeyTypePublic) + throw std::runtime_error("Invalid type public to be exported"); + + ManagedEVPPKey m_pkey = key_data->GetAsymmetricKey(); + if (EVP_PKEY_id(m_pkey.get()) != EVP_PKEY_EC) { + PKEY_SPKI_Export(key_data.get(), out); + return; + } else { + // Ensure exported key is in uncompressed point format. + // The temporary EC key is so we can have i2d_PUBKEY_bio() write out + // the header but it is a somewhat silly hoop to jump through because + // the header is for all practical purposes a static 26 byte sequence + // where only the second byte changes. + + const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(m_pkey.get()); + const EC_GROUP* group = EC_KEY_get0_group(ec_key); + const EC_POINT* point = EC_KEY_get0_public_key(ec_key); + const point_conversion_form_t form = + POINT_CONVERSION_UNCOMPRESSED; + const size_t need = + EC_POINT_point2oct(group, point, form, nullptr, 0, nullptr); + if (need == 0) { + throw std::runtime_error("Failed to export EC key"); + } + ByteSource::Builder data(need); + const size_t have = EC_POINT_point2oct(group, + point, form, data.data(), need, nullptr); + if (have == 0) { + throw std::runtime_error("Failed to export EC key"); + } + ECKeyPointer ec(EC_KEY_new()); + CHECK_EQ(1, EC_KEY_set_group(ec.get(), group)); + ECPointPointer uncompressed(EC_POINT_new(group)); + CHECK_EQ(1, + EC_POINT_oct2point(group, + uncompressed.get(), + data.data(), + data.size(), + nullptr)); + CHECK_EQ(1, EC_KEY_set_public_key(ec.get(), + uncompressed.get())); + EVPKeyPointer pkey(EVP_PKEY_new()); + CHECK_EQ(1, EVP_PKEY_set1_EC_KEY(pkey.get(), ec.get())); + BIOPointer bio(BIO_new(BIO_s_mem())); + CHECK(bio); + if (!i2d_PUBKEY_bio(bio.get(), pkey.get())) { + throw std::runtime_error("Failed to export EC key"); + } + *out = ByteSource::FromBIO(bio); + return; + } + } + default: + throw std::runtime_error("Un-reachable export code");; + } +} + +} // namespace margelo diff --git a/cpp/webcrypto/crypto_ec.h b/cpp/webcrypto/crypto_ec.h new file mode 100644 index 00000000..bc766ca4 --- /dev/null +++ b/cpp/webcrypto/crypto_ec.h @@ -0,0 +1,41 @@ +// +// crypto_ec.hpp +// BEMCheckBox +// +// Created by Oscar Franco on 30/11/23. +// + +#ifndef crypto_ec_h +#define crypto_ec_h + +#include +#include +#include +#include "MGLKeys.h" +#ifdef ANDROID +#include "Utils/MGLUtils.h" +#include "webcrypto/MGLWebCrypto.h" +#else +#include "MGLUtils.h" +#include "MGLWebCrypto.h" +#endif + + +namespace margelo { +namespace jsi = facebook::jsi; + +class ECDH final { + public: + static ECPointPointer BufferToPoint(jsi::Runtime &rt, + const EC_GROUP* group, + jsi::ArrayBuffer &buf); + static void doExport(jsi::Runtime &rt, + WebCryptoKeyFormat format, + std::shared_ptr key_data, + ByteSource* out); +}; + +} // namespace margelo + + +#endif /* crypto_ec_hpp */ diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 611d937e..d28df6ce 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,93 +1,13 @@ apply plugin: "com.android.application" apply plugin: "com.facebook.react" -import com.android.build.OutputFile - -/** - * This is the configuration block to customize your React Native Android app. - * By default you don't need to apply any configuration, just uncomment the lines you need. - */ react { - /* Folders */ - // The root of your project, i.e. where "package.json" lives. Default is '..' - // root = file("../") - // The folder where the react-native NPM package is. Default is ../node_modules/react-native - // reactNativeDir = file("../node_modules/react-native") - // The folder where the react-native Codegen package is. Default is ../node_modules/react-native-codegen - // codegenDir = file("../node_modules/react-native-codegen") - // The cli.js file which is the React Native CLI entrypoint. Default is ../node_modules/react-native/cli.js - // cliFile = file("../node_modules/react-native/cli.js") - - /* Variants */ - // The list of variants to that are debuggable. For those we're going to - // skip the bundling of the JS bundle and the assets. By default is just 'debug'. - // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. - // debuggableVariants = ["liteDebug", "prodDebug"] - - /* Bundling */ - // A list containing the node command and its flags. Default is just 'node'. - // nodeExecutableAndArgs = ["node"] - // - // The command to run when bundling. By default is 'bundle' - // bundleCommand = "ram-bundle" - // - // The path to the CLI configuration file. Default is empty. - // bundleConfig = file(../rn-cli.config.js) - // - // The name of the generated asset file containing your JS bundle - // bundleAssetName = "MyApplication.android.bundle" - // - // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' - // entryFile = file("../js/MyApplication.android.js") - // - // A list of extra flags to pass to the 'bundle' commands. - // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle - // extraPackagerArgs = [] - - /* Hermes Commands */ - // The hermes compiler command to run. By default it is 'hermesc' - // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" - // - // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" - // hermesFlags = ["-O", "-output-source-map"] } -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false - -/** - * Set this to true to Run Proguard on Release builds to minify the Java bytecode. - */ def enableProguardInReleaseBuilds = false -/** - * The preferred build flavor of JavaScriptCore (JSC) - * - * For example, to use the international variant, you can use: - * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` - * - * The international variant includes ICU i18n library and necessary data - * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that - * give correct results when using with locales other than en-US. Note that - * this variant is about 6MiB larger per architecture than default. - */ def jscFlavor = 'org.webkit:android-jsc:+' -/** - * Private function to get the list of Native Architectures you want to build. - * This reads the value from reactNativeArchitectures in your gradle.properties - * file and works together with the --active-arch-only flag of react-native run-android. - */ -def reactNativeArchitectures() { - def value = project.getProperties().get("reactNativeArchitectures") - return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] -} - android { ndkVersion rootProject.ext.ndkVersion @@ -102,14 +22,6 @@ android { versionName "1.0" } - splits { - abi { - reset() - enable enableSeparateBuildPerCPUArchitecture - universalApk false // If true, also generate a universal APK - include (*reactNativeArchitectures()) - } - } signingConfigs { debug { storeFile file('debug.keystore') @@ -130,22 +42,6 @@ android { proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } } - - // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> - variant.outputs.each { output -> - // For each separate APK per architecture, set a unique version code as described here: - // https://developer.android.com/studio/build/configure-apk-splits.html - // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. - def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) - if (abi != null) { // null for the universal-debug, universal-release variants - output.versionCodeOverride = - defaultConfig.versionCode * 1000 + versionCodes.get(abi) - } - - } - } } dependencies { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index f280aad2..2ab55961 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -317,7 +317,7 @@ PODS: - React-jsinspector (0.72.7) - React-logger (0.72.7): - glog - - react-native-quick-base64 (2.0.5): + - react-native-quick-base64 (2.0.7): - React-Core - react-native-quick-crypto (0.6.1): - OpenSSL-Universal @@ -325,12 +325,8 @@ PODS: - React-callinvoker - React-Core - ReactCommon - - react-native-safe-area-context (4.5.0): - - RCT-Folly - - RCTRequired - - RCTTypeSafety + - react-native-safe-area-context (4.7.4): - React-Core - - ReactCommon/turbomodule/core - React-NativeModulesApple (0.72.7): - hermes-engine - React-callinvoker @@ -459,9 +455,9 @@ PODS: - RNCCheckbox (0.5.16): - BEMCheckBox (~> 1.4) - React-Core - - RNScreens (3.20.0): + - RNScreens (3.27.0): + - RCT-Folly (= 2021.07.22.00) - React-Core - - React-RCTImage - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -638,9 +634,9 @@ SPEC CHECKSUMS: React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f - react-native-quick-base64: e657e9197e61b60a9dec49807843052b830da254 + react-native-quick-base64: a5dbe4528f1453e662fcf7351029500b8b63e7bb react-native-quick-crypto: 82eedfdbd352c8fa861512f80f07d069c411c9b8 - react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc + react-native-safe-area-context: 2cd91d532de12acdb0a9cbc8d43ac72a8e4c897c React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d @@ -659,7 +655,7 @@ SPEC CHECKSUMS: React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415 ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6 RNCCheckbox: 75255b03e604bbcc26411eb31c4cbe3e3865f538 - RNScreens: 218801c16a2782546d30bd2026bb625c0302d70f + RNScreens: 3c2d122f5e08c192e254c510b212306da97d2581 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 diff --git a/example/src/components/TestItem.tsx b/example/src/components/TestItem.tsx index 2b8bbfef..60f1ba05 100644 --- a/example/src/components/TestItem.tsx +++ b/example/src/components/TestItem.tsx @@ -17,13 +17,13 @@ export const TestItem: React.FC = ({ }: TestItemProps) => { return ( - {description} { onToggle(index); }} /> + {description} ); }; @@ -36,7 +36,12 @@ const styles = StyleSheet.create({ alignContent: 'center', alignItems: 'center', justifyContent: 'space-evenly', - backgroundColor: '#99CCFF', marginTop: 10, + gap: 20, + borderBottomWidth: 1, + borderBottomColor: '#ccc', + }, + label: { + flex: 1, }, }); diff --git a/example/src/testing/TestList.ts b/example/src/testing/TestList.ts index 9fed5274..f7ce3274 100644 --- a/example/src/testing/TestList.ts +++ b/example/src/testing/TestList.ts @@ -9,8 +9,14 @@ import { registerConstantsTest } from './Tests/ConstantsTest/ConstantsTest'; import { registerPublicCipherTests } from './Tests/CipherTests/PublicCipherTests'; import { registerGenerateKeyPairTests } from './Tests/CipherTests/GenerateKeyPairTests'; import { registerSignTests } from './Tests/SignTests/SignTests'; +import { webcryptoRegisterTests } from './Tests/webcryptoTests/webcryptoTests'; export const TEST_LIST: Array = [ + { + description: 'webcrypto', + value: false, + registrator: webcryptoRegisterTests, + }, { description: 'PBKDF2', value: false, diff --git a/example/src/testing/Tests/CipherTests/PublicCipherTests.ts b/example/src/testing/Tests/CipherTests/PublicCipherTests.ts index 6fac1eeb..b616d385 100644 --- a/example/src/testing/Tests/CipherTests/PublicCipherTests.ts +++ b/example/src/testing/Tests/CipherTests/PublicCipherTests.ts @@ -50,7 +50,6 @@ export function registerPublicCipherTests() { const decrypted = privateKey.decrypt(encrypted); chai.expect(decrypted.toString('utf-8')).to.equal(clearText); } catch (e) { - console.warn('error', e); chai.assert.fail(); } }); diff --git a/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts b/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts new file mode 100644 index 00000000..6e789fd4 --- /dev/null +++ b/example/src/testing/Tests/webcryptoTests/webcryptoTests.ts @@ -0,0 +1,82 @@ +import chai from 'chai'; +import { atob, btoa } from 'react-native-quick-base64'; +import crypto from 'react-native-quick-crypto'; +import { it } from '../../MochaRNAdapter'; + +// Tests that a key pair can be used for encryption / decryption. +// function testEncryptDecrypt(publicKey: any, privateKey: any) { +// const message = 'Hello Node.js world!'; +// const plaintext = Buffer.from(message, 'utf8'); +// for (const key of [publicKey, privateKey]) { +// const ciphertext = crypto.publicEncrypt(key, plaintext); +// const received = crypto.privateDecrypt(privateKey, ciphertext); +// chai.assert.strictEqual(received.toString('utf8'), message); +// } +// } + +// I guess interally this functions use privateEncrypt/publicDecrypt (sign/verify) +// but the main function `sign` is not implemented yet +// Tests that a key pair can be used for signing / verification. +// function testSignVerify(publicKey: any, privateKey: any) { +// const message = Buffer.from('Hello Node.js world!'); + +// function oldSign(algo, data, key) { +// return createSign(algo).update(data).sign(key); +// } + +// function oldVerify(algo, data, key, signature) { +// return createVerify(algo).update(data).verify(key, signature); +// } + +// for (const signFn of [sign, oldSign]) { +// const signature = signFn('SHA256', message, privateKey); +// for (const verifyFn of [verify, oldVerify]) { +// for (const key of [publicKey, privateKey]) { +// const okay = verifyFn('SHA256', message, key, signature); +// assert(okay); +// } +// } +// } +// } + +function base64ToArrayBuffer(val: string): ArrayBuffer { + var binaryString = atob(val); + var bytes = new Uint8Array(binaryString.length); + for (var i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; +} + +function arrayBufferToBase64(buffer: ArrayBuffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]!); + } + return btoa(binary); +} + +export function webcryptoRegisterTests() { + it('EC import raw/export SPKI', async () => { + const key = await crypto.subtle.importKey( + 'raw', + base64ToArrayBuffer( + 'BDZRaWzATXwmOi4Y/QP3JXn8sSVSFxidMugnGf3G28snm7zek9GjT76UMhXVMEbWLxR5WG6iGTjPAKKnT3J0jCA=' + ), + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['verify'] + ); + + const buf = await crypto.subtle.exportKey('spki', key); + const spkiKey = arrayBufferToBase64(buf); + console.warn(spkiKey); + chai + .expect(spkiKey) + .to.equal( + 'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENlFpbMBNfCY6Lhj9A/clefyxJVIXGJ0y6CcZ/cbbyyebvN6T0aNPvpQyFdUwRtYvFHlYbqIZOM8AoqdPcnSMIA==' + ); + }); +} diff --git a/src/NativeQuickCrypto/NativeQuickCrypto.ts b/src/NativeQuickCrypto/NativeQuickCrypto.ts index 32f5ebea..b6310e36 100644 --- a/src/NativeQuickCrypto/NativeQuickCrypto.ts +++ b/src/NativeQuickCrypto/NativeQuickCrypto.ts @@ -12,6 +12,7 @@ import type { GenerateKeyPairSyncMethod, } from './Cipher'; import type { CreateSignMethod, CreateVerifyMethod } from './sig'; +import type { webcrypto } from './webcrypto'; interface NativeQuickCryptoSpec { createHmac: CreateHmacMethod; @@ -27,6 +28,7 @@ interface NativeQuickCryptoSpec { generateKeyPairSync: GenerateKeyPairSyncMethod; createSign: CreateSignMethod; createVerify: CreateVerifyMethod; + webcrypto: webcrypto; } // global func declaration for JSI functions diff --git a/src/NativeQuickCrypto/webcrypto.ts b/src/NativeQuickCrypto/webcrypto.ts new file mode 100644 index 00000000..d310fb00 --- /dev/null +++ b/src/NativeQuickCrypto/webcrypto.ts @@ -0,0 +1,17 @@ +import type { KWebCryptoKeyFormat } from '../keys'; + +type ECExportKey = ( + format: KWebCryptoKeyFormat, + handle: KeyObjectHandle +) => ArrayBuffer; + +export type KeyObjectHandle = { + initECRaw(curveName: string, keyData: ArrayBuffer): boolean; +}; + +type CreateKeyObjectHandle = () => KeyObjectHandle; + +export type webcrypto = { + ecExportKey: ECExportKey; + createKeyObjectHandle: CreateKeyObjectHandle; +}; diff --git a/src/QuickCrypto.ts b/src/QuickCrypto.ts index 54df279b..b7840408 100644 --- a/src/QuickCrypto.ts +++ b/src/QuickCrypto.ts @@ -15,6 +15,7 @@ import { createSign, createVerify } from './sig'; import { createHmac } from './Hmac'; import { createHash } from './Hash'; import { constants } from './constants'; +import { subtle } from './subtle'; export const QuickCrypto = { createHmac, @@ -32,6 +33,7 @@ export const QuickCrypto = { generateKeyPairSync, createSign, createVerify, + subtle, constants, ...pbkdf2, ...random, diff --git a/src/Utils.ts b/src/Utils.ts index 5858262e..82ae44dd 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,5 +1,6 @@ import { Buffer } from '@craftzdog/react-native-buffer'; +export type BufferLike = ArrayBuffer | Buffer | ArrayBufferView; export type BinaryLike = string | ArrayBuffer | Buffer; export type BinaryToTextEncoding = 'base64' | 'base64url' | 'hex' | 'binary'; @@ -109,6 +110,14 @@ export function toArrayBuffer(buf: Buffer): ArrayBuffer { return ab; } +export function bufferLikeToArrayBuffer(buf: BufferLike): ArrayBuffer { + return Buffer.isBuffer(buf) + ? buf.buffer + : ArrayBuffer.isView(buf) + ? buf.buffer + : buf; +} + export function binaryLikeToArrayBuffer( input: BinaryLike, encoding: string = 'utf-8' diff --git a/src/ec.ts b/src/ec.ts new file mode 100644 index 00000000..0f40367e --- /dev/null +++ b/src/ec.ts @@ -0,0 +1,321 @@ +import { NativeQuickCrypto } from './NativeQuickCrypto/NativeQuickCrypto'; +import { bufferLikeToArrayBuffer, type BufferLike } from './Utils'; +import { + type ImportFormat, + type SubtleAlgorithm, + type KeyUsage, + kNamedCurveAliases, + type NamedCurve, + PublicKeyObject, + KWebCryptoKeyFormat, + CryptoKey, +} from './keys'; + +// const { +// ArrayPrototypeIncludes, +// ObjectKeys, +// SafeSet, +// } = primordials; + +// const { +// ECKeyExportJob, +// KeyObjectHandle, +// SignJob, +// kCryptoJobAsync, +// kKeyTypePrivate, +// kSignJobModeSign, +// kSignJobModeVerify, +// kSigEncP1363, +// } = internalBinding('crypto'); + +// const { +// getUsagesUnion, +// hasAnyNotIn, +// jobPromise, +// normalizeHashName, +// validateKeyOps, +// kHandle, +// kKeyObject, +// kNamedCurveAliases, +// } = require('internal/crypto/util'); + +// const { +// lazyDOMException, +// promisify, +// } = require('internal/util'); + +// const { +// generateKeyPair: _generateKeyPair, +// } = require('internal/crypto/keygen'); + +// const { +// InternalCryptoKey, +// PrivateKeyObject, +// PublicKeyObject, +// createPrivateKey, +// createPublicKey, +// } = require('internal/crypto/keys'); + +// const generateKeyPair = promisify(_generateKeyPair); + +// function verifyAcceptableEcKeyUse(name, isPublic, usages) { +// let checkSet; +// switch (name) { +// case 'ECDH': +// checkSet = isPublic ? [] : ['deriveKey', 'deriveBits']; +// break; +// case 'ECDSA': +// checkSet = isPublic ? ['verify'] : ['sign']; +// break; +// default: +// throw lazyDOMException( +// 'The algorithm is not supported', 'NotSupportedError'); +// } +// if (hasAnyNotIn(usages, checkSet)) { +// throw lazyDOMException( +// `Unsupported key usage for a ${name} key`, +// 'SyntaxError'); +// } +// } + +function createECPublicKeyRaw( + namedCurve: NamedCurve, + keyData: ArrayBuffer +): PublicKeyObject { + const handle = NativeQuickCrypto.webcrypto.createKeyObjectHandle(); + if (!handle.initECRaw(kNamedCurveAliases[namedCurve], keyData)) { + throw new Error('Invalid keyData'); + } + + return new PublicKeyObject(handle); +} + +// async function ecGenerateKey(algorithm, extractable, keyUsages) { +// const { name, namedCurve } = algorithm; + +// if (!ArrayPrototypeIncludes(ObjectKeys(kNamedCurveAliases), namedCurve)) { +// throw lazyDOMException( +// 'Unrecognized namedCurve', +// 'NotSupportedError'); +// } + +// const usageSet = new SafeSet(keyUsages); +// switch (name) { +// case 'ECDSA': +// if (hasAnyNotIn(usageSet, ['sign', 'verify'])) { +// throw lazyDOMException( +// 'Unsupported key usage for an ECDSA key', +// 'SyntaxError'); +// } +// break; +// case 'ECDH': +// if (hasAnyNotIn(usageSet, ['deriveKey', 'deriveBits'])) { +// throw lazyDOMException( +// 'Unsupported key usage for an ECDH key', +// 'SyntaxError'); +// } +// // Fall through +// } + +// const keypair = await generateKeyPair('ec', { namedCurve }).catch((err) => { +// throw lazyDOMException( +// 'The operation failed for an operation-specific reason', +// { name: 'OperationError', cause: err }); +// }); + +// let publicUsages; +// let privateUsages; +// switch (name) { +// case 'ECDSA': +// publicUsages = getUsagesUnion(usageSet, 'verify'); +// privateUsages = getUsagesUnion(usageSet, 'sign'); +// break; +// case 'ECDH': +// publicUsages = []; +// privateUsages = getUsagesUnion(usageSet, 'deriveKey', 'deriveBits'); +// break; +// } + +// const keyAlgorithm = { name, namedCurve }; + +// const publicKey = +// new InternalCryptoKey( +// keypair.publicKey, +// keyAlgorithm, +// publicUsages, +// true); + +// const privateKey = +// new InternalCryptoKey( +// keypair.privateKey, +// keyAlgorithm, +// privateUsages, +// extractable); + +// return { __proto__: null, publicKey, privateKey }; +// } + +export function ecExportKey( + key: CryptoKey, + format: KWebCryptoKeyFormat +): ArrayBuffer { + return NativeQuickCrypto.webcrypto.ecExportKey(format, key.keyObject.handle); +} + +export function ecImportKey( + format: ImportFormat, + keyData: BufferLike, + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[] +) { + const { name, namedCurve } = algorithm; + + // if (!ArrayPrototypeIncludes(ObjectKeys(kNamedCurveAliases), namedCurve)) { + // throw lazyDOMException('Unrecognized namedCurve', 'NotSupportedError'); + // } + + let keyObject; + // const usagesSet = new SafeSet(keyUsages); + switch (format) { + // case 'spki': { + // // verifyAcceptableEcKeyUse(name, true, usagesSet); + // try { + // keyObject = createPublicKey({ + // key: keyData, + // format: 'der', + // type: 'spki', + // }); + // } catch (err) { + // throw new Error(`Invalid keyData: ${err}`); + // } + // break; + // } + // case 'pkcs8': { + // // verifyAcceptableEcKeyUse(name, false, usagesSet); + // try { + // keyObject = createPrivateKey({ + // key: keyData, + // format: 'der', + // type: 'pkcs8', + // }); + // } catch (err) { + // throw new Error(`Invalid keyData ${err}`); + // } + // break; + // } + // case 'jwk': { + // if (!keyData.kty) throw lazyDOMException('Invalid keyData', 'DataError'); + // if (keyData.kty !== 'EC') + // throw lazyDOMException('Invalid JWK "kty" Parameter', 'DataError'); + // if (keyData.crv !== namedCurve) + // throw lazyDOMException( + // 'JWK "crv" does not match the requested algorithm', + // 'DataError' + // ); + + // verifyAcceptableEcKeyUse(name, keyData.d === undefined, usagesSet); + + // if (usagesSet.size > 0 && keyData.use !== undefined) { + // const checkUse = name === 'ECDH' ? 'enc' : 'sig'; + // if (keyData.use !== checkUse) + // throw lazyDOMException('Invalid JWK "use" Parameter', 'DataError'); + // } + + // validateKeyOps(keyData.key_ops, usagesSet); + + // if ( + // keyData.ext !== undefined && + // keyData.ext === false && + // extractable === true + // ) { + // throw lazyDOMException( + // 'JWK "ext" Parameter and extractable mismatch', + // 'DataError' + // ); + // } + + // if (algorithm.name === 'ECDSA' && keyData.alg !== undefined) { + // let algNamedCurve; + // switch (keyData.alg) { + // case 'ES256': + // algNamedCurve = 'P-256'; + // break; + // case 'ES384': + // algNamedCurve = 'P-384'; + // break; + // case 'ES512': + // algNamedCurve = 'P-521'; + // break; + // } + // if (algNamedCurve !== namedCurve) + // throw lazyDOMException( + // 'JWK "alg" does not match the requested algorithm', + // 'DataError' + // ); + // } + + // const handle = new KeyObjectHandle(); + // const type = handle.initJwk(keyData, namedCurve); + // if (type === undefined) + // throw lazyDOMException('Invalid JWK', 'DataError'); + // keyObject = + // type === kKeyTypePrivate + // ? new PrivateKeyObject(handle) + // : new PublicKeyObject(handle); + // break; + // } + case 'raw': { + // verifyAcceptableEcKeyUse(name, true, usagesSet); + let buffer = bufferLikeToArrayBuffer(keyData); + keyObject = createECPublicKeyRaw(namedCurve, buffer); + break; + } + default: { + throw new Error('Unknown format'); + } + } + + switch (algorithm.name) { + case 'ECDSA': + // Fall through + case 'ECDH': + // if (keyObject.asymmetricKeyType !== 'ec') + // throw new Error('Invalid key type'); + break; + } + + // if (!keyObject[kHandle].checkEcKeyData()) { + // throw new Error('Invalid keyData'); + // } + + // const { namedCurve: checkNamedCurve } = keyObject[kHandle].keyDetail({}); + // if (kNamedCurveAliases[namedCurve] !== checkNamedCurve) + // throw new Error('Named curve mismatch'); + + 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)); +// } diff --git a/src/keys.ts b/src/keys.ts index bc0f9d05..6bb8abb7 100644 --- a/src/keys.ts +++ b/src/keys.ts @@ -3,6 +3,30 @@ import { binaryLikeToArrayBuffer, isStringOrBuffer, } from './Utils'; +import type { KeyObjectHandle } from './NativeQuickCrypto/webcrypto'; + +export const kNamedCurveAliases = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1', +} as const; + +export type NamedCurve = 'P-256' | 'P-384' | 'P-521'; + +export type ImportFormat = 'raw' | 'pkcs8' | 'spki' | 'jwk'; +export type SubtleAlgorithm = { + name: 'ECDSA' | 'ECDH'; + namedCurve: NamedCurve; +}; +export type KeyUsage = + | 'encrypt' + | 'decrypt' + | 'sign' + | 'verify' + | 'deriveKey' + | 'deriveBits' + | 'wrapKey' + | 'unwrapKey'; // On node this value is defined on the native side, for now I'm just creating it here in JS // TODO(osp) move this into native side to make sure they always match @@ -12,6 +36,14 @@ enum KFormatType { kKeyFormatJWK, } +// Same as KFormatType, this enum needs to be defined on the native side +export enum KWebCryptoKeyFormat { + kWebCryptoKeyFormatRaw, + kWebCryptoKeyFormatPKCS8, + kWebCryptoKeyFormatSPKI, + kWebCryptoKeyFormatJWK, +} + enum KeyInputContext { kConsumePublic, kConsumePrivate, @@ -304,3 +336,229 @@ export function parsePrivateKeyEncoding( ) { return parseKeyEncoding(enc, keyType, false, objName); } + +export class CryptoKey { + keyObject: PublicKeyObject; + algorithm: SubtleAlgorithm; + keyUsages: KeyUsage[]; + extractable: boolean; + + constructor( + keyObject: KeyObject, + algorithm: SubtleAlgorithm, + keyUsages: KeyUsage[], + extractable: boolean + ) { + this.keyObject = keyObject; + this.algorithm = algorithm; + this.keyUsages = keyUsages; + this.extractable = extractable; + } + + inspect(_depth: number, _options: any): any { + throw new Error('NOT IMPLEMENTED'); + // if (depth < 0) return this; + + // const opts = { + // ...options, + // depth: options.depth == null ? null : options.depth - 1, + // }; + + // return `CryptoKey ${inspect( + // { + // type: this.type, + // extractable: this.extractable, + // algorithm: this.algorithm, + // usages: this.usages, + // }, + // opts + // )}`; + } + + get type() { + // if (!(this instanceof CryptoKey)) throw new Error('Invalid CryptoKey'); + return this.keyObject.type; + } + + // get extractable() { + // if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); + // return this[kExtractable]; + // } + + // get algorithm() { + // if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); + // return this[kAlgorithm]; + // } + + // get usages() { + // if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS('CryptoKey'); + // return ArrayFrom(this[kKeyUsages]); + // } +} + +// ObjectDefineProperties(CryptoKey.prototype, { +// type: kEnumerableProperty, +// extractable: kEnumerableProperty, +// algorithm: kEnumerableProperty, +// usages: kEnumerableProperty, +// [SymbolToStringTag]: { +// __proto__: null, +// configurable: true, +// value: 'CryptoKey', +// }, +// }); + +class KeyObject { + handle: KeyObjectHandle; + type: 'public' | 'secret' | 'private' | 'unknown' = 'unknown'; + + constructor(type: string, handle: KeyObjectHandle) { + if (type !== 'secret' && type !== 'public' && type !== 'private') + throw new Error(`type: ${type}`); + this.handle = handle; + this.type = type; + } + + // get type(): string { + // return this.type; + // } + + // static from(key) { + // if (!isCryptoKey(key)) + // throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); + // return key[kKeyObject]; + // } + + // equals(otherKeyObject) { + // if (!isKeyObject(otherKeyObject)) { + // throw new ERR_INVALID_ARG_TYPE( + // 'otherKeyObject', + // 'KeyObject', + // otherKeyObject + // ); + // } + + // return ( + // otherKeyObject.type === this.type && + // this[kHandle].equals(otherKeyObject[kHandle]) + // ); + // } +} + +// ObjectDefineProperties(KeyObject.prototype, { +// [SymbolToStringTag]: { +// __proto__: null, +// configurable: true, +// value: 'KeyObject', +// }, +// }); + +export class SecretKeyObject extends KeyObject { + constructor(handle: KeyObjectHandle) { + super('secret', handle); + } + + // get symmetricKeySize() { + // return this[kHandle].getSymmetricKeySize(); + // } + + // export(options) { + // if (options !== undefined) { + // validateObject(options, 'options'); + // validateOneOf(options.format, 'options.format', [ + // undefined, + // 'buffer', + // 'jwk', + // ]); + // if (options.format === 'jwk') { + // return this[kHandle].exportJwk({}, false); + // } + // } + // return this[kHandle].export(); + // } +} + +// const kAsymmetricKeyType = Symbol('kAsymmetricKeyType'); +// const kAsymmetricKeyDetails = Symbol('kAsymmetricKeyDetails'); + +// function normalizeKeyDetails(details = {}) { +// if (details.publicExponent !== undefined) { +// return { +// ...details, +// publicExponent: bigIntArrayToUnsignedBigInt( +// new Uint8Array(details.publicExponent) +// ), +// }; +// } +// return details; +// } + +class AsymmetricKeyObject extends KeyObject { + constructor(type: string, handle: KeyObjectHandle) { + super(type, handle); + } + + // get asymmetricKeyType() { + // return ( + // this[kAsymmetricKeyType] || + // (this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType()) + // ); + // } + + // get asymmetricKeyDetails() { + // switch (this.asymmetricKeyType) { + // case 'rsa': + // case 'rsa-pss': + // case 'dsa': + // case 'ec': + // return ( + // this[kAsymmetricKeyDetails] || + // (this[kAsymmetricKeyDetails] = normalizeKeyDetails( + // this[kHandle].keyDetail({}) + // )) + // ); + // default: + // return {}; + // } + // } +} + +export class PublicKeyObject extends AsymmetricKeyObject { + constructor(handle: KeyObjectHandle) { + super('public', handle); + } + + // export(options: any) { + // if (options && options.format === 'jwk') { + // return this[kHandle].exportJwk({}, false); + // } + // const { format, type } = parsePublicKeyEncoding( + // options, + // this.asymmetricKeyType + // ); + // return this[kHandle].export(format, type); + // } +} + +export class PrivateKeyObject extends AsymmetricKeyObject { + constructor(handle: KeyObjectHandle) { + super('private', handle); + } + + // export(options) { + // if (options && options.format === 'jwk') { + // if (options.passphrase !== undefined) { + // throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( + // 'jwk', + // 'does not support encryption' + // ); + // } + // return this[kHandle].exportJwk({}, false); + // } + // const { format, type, cipher, passphrase } = parsePrivateKeyEncoding( + // options, + // this.asymmetricKeyType + // ); + // return this[kHandle].export(format, type, cipher, passphrase); + // } +} diff --git a/src/subtle.ts b/src/subtle.ts new file mode 100644 index 00000000..c630eb1f --- /dev/null +++ b/src/subtle.ts @@ -0,0 +1,171 @@ +import { + type ImportFormat, + type SubtleAlgorithm, + type KeyUsage, + CryptoKey, + KWebCryptoKeyFormat, +} from './keys'; +import { ecImportKey, ecExportKey } from './ec'; +import type { BufferLike } from './Utils'; + +async function exportKeySpki(key: CryptoKey): Promise { + switch (key.algorithm.name) { + // case 'RSASSA-PKCS1-v1_5': + // // Fall through + // case 'RSA-PSS': + // // Fall through + // case 'RSA-OAEP': + // if (key.type === 'public') { + // return require('internal/crypto/rsa').rsaExportKey( + // key, + // kWebCryptoKeyFormatSPKI + // ); + // } + // break; + case 'ECDSA': + // Fall through + case 'ECDH': + if (key.type === 'public') { + return ecExportKey(key, KWebCryptoKeyFormat.kWebCryptoKeyFormatSPKI); + } + break; + // case 'Ed25519': + // // Fall through + // case 'Ed448': + // // Fall through + // case 'X25519': + // // Fall through + // case 'X448': + // if (key.type === 'public') { + // return require('internal/crypto/cfrg').cfrgExportKey( + // key, + // kWebCryptoKeyFormatSPKI + // ); + // } + // break; + } + + throw new Error( + `Unable to export a raw ${key.algorithm.name} ${key.type} key` + ); +} + +class Subtle { + async importKey( + format: ImportFormat, + data: BufferLike, + algorithm: SubtleAlgorithm, + extractable: boolean, + keyUsages: KeyUsage[] + ): Promise { + let result: CryptoKey; + switch (algorithm.name) { + // case 'RSASSA-PKCS1-v1_5': + // // Fall through + // case 'RSA-PSS': + // // Fall through + // case 'RSA-OAEP': + // result = await require('internal/crypto/rsa').rsaImportKey( + // format, + // keyData, + // algorithm, + // extractable, + // keyUsages + // ); + // break; + case 'ECDSA': + // Fall through + case 'ECDH': + result = await ecImportKey( + format, + data, + algorithm, + extractable, + keyUsages + ); + break; + // case 'Ed25519': + // // Fall through + // case 'Ed448': + // // Fall through + // case 'X25519': + // // Fall through + // case 'X448': + // result = await require('internal/crypto/cfrg').cfrgImportKey( + // format, + // keyData, + // algorithm, + // extractable, + // keyUsages + // ); + // break; + // case 'HMAC': + // result = await require('internal/crypto/mac').hmacImportKey( + // format, + // keyData, + // algorithm, + // extractable, + // keyUsages + // ); + // break; + // case 'AES-CTR': + // // Fall through + // case 'AES-CBC': + // // Fall through + // case 'AES-GCM': + // // Fall through + // case 'AES-KW': + // result = await require('internal/crypto/aes').aesImportKey( + // algorithm, + // format, + // keyData, + // extractable, + // keyUsages + // ); + // break; + // case 'HKDF': + // // Fall through + // case 'PBKDF2': + // result = await importGenericSecretKey( + // algorithm, + // format, + // keyData, + // extractable, + // keyUsages + // ); + // break; + default: + throw new Error(`Unrecognized algorithm name ${algorithm.name}`); + } + + // if ( + // (result.type === 'secret' || result.type === 'private') && + // result.usages.length === 0 + // ) { + // throw new Error( + // `Usages cannot be empty when importing a ${result.type} key.` + // ); + // } + + return result; + } + + async exportKey( + format: ImportFormat, + key: CryptoKey + ): Promise { + if (!key.extractable) throw new Error('key is not extractable'); + + switch (format) { + case 'spki': + return await exportKeySpki(key); + // case 'jwk': + // return exportKeyJWK(key); + // case 'raw': + // return exportKeyRaw(key); + } + throw new Error('Export format is unsupported'); + } +} + +export const subtle = new Subtle();