Skip to content

Commit

Permalink
Implement webcrypto import ECDSA key and export as SPKI (#200)
Browse files Browse the repository at this point in the history
* Update README

* Make test UI a bit nicer and fix an unhandled error on randomFill

* Fix some tests and some types

* Add a guard for buffer enconding

* Add type to import pbkdf2

* Import almost working

* Add crypto_ec. file

* Working internal openSLL key generatin

* Compiling C++ implementation of import EC key and fix JS classes and types

* Minor type fixes

* Simplify test structure

* Simplify module

* Working ECDSA import and SPKI export

* Typescript fixes

* Add types to parent package

* Dependencies

* Update gradle wrapper

* Update Java version on action

* Update Java version on action

* Update Java version on action

* Remove yarn.lock changes action

* Do not use build caches

* Do not use build caches

* Change react native android dependency

* Fix Android compilation

* Change lint to run on example folder

* Change lint to run on example folder

* Change lint to run on example folder

* Change lint to run on example folder

* Adjust Android compilation

* JS linting

* cpp linting

* Fix android header

* Update Lint Report action version

* Update Lint Report action version

* Update JDK Setup

* Update dependencies

* Update dependencies

* linting

* Reinsert deps

* lock

* lock

* Simplify example app build.gradle

* Add pickFirst instructions to the README

* remove pickFirst from build.gradle
  • Loading branch information
ospfranco authored Jan 3, 2024
1 parent 0cea276 commit 430ed77
Show file tree
Hide file tree
Showing 27 changed files with 1,475 additions and 315 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions android/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
8 changes: 3 additions & 5 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions cpp/JSIUtils/MGLJSIMacros.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
111 changes: 71 additions & 40 deletions cpp/MGLKeys.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#include <jsi/jsi.h>
#include <openssl/bio.h>
#include <openssl/ec.h>

#include <algorithm>
#include <optional>
Expand All @@ -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 {
Expand Down Expand Up @@ -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> KeyObjectData::CreateSecret(ByteSource key)
// {
// CHECK(key);
// return std::shared_ptr<KeyObjectData>(new KeyObjectData(std::move(key)));
// }
//
// std::shared_ptr<KeyObjectData> KeyObjectData::CreateAsymmetric(
// KeyType
// key_type,
// const
// ManagedEVPPKey&
// pkey) {
// CHECK(pkey);
// return std::shared_ptr<KeyObjectData>(new KeyObjectData(key_type, pkey));
// }
//
// KeyType KeyObjectData::GetKeyType() const {
// return key_type_;
// }
//
// ManagedEVPPKey KeyObjectData::GetAsymmetricKey() const {

std::shared_ptr<KeyObjectData> KeyObjectData::CreateAsymmetric(
KeyType key_type,
const ManagedEVPPKey& pkey
) {
CHECK(pkey);
return std::shared_ptr<KeyObjectData>(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<char>();
Expand Down Expand Up @@ -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<Value>& args) {
// Environment* env = Environment::GetCurrent(args);
Expand Down Expand Up @@ -1456,4 +1486,5 @@ ManagedEVPPKey ManagedEVPPKey::GetParsedKey(jsi::Runtime& runtime,
// void RegisterExternalReferences(ExternalReferenceRegistry * registry) {
// KeyObjectHandle::RegisterExternalReferences(registry);
// }

} // namespace margelo
43 changes: 43 additions & 0 deletions cpp/MGLKeys.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<KeyObjectData> CreateSecret(ByteSource key);

static std::shared_ptr<KeyObjectData> 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<KeyObjectData> data_;
};

} // namespace margelo

#endif /* MGLCipherKeys_h */
13 changes: 8 additions & 5 deletions cpp/MGLQuickCryptoHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -34,6 +35,7 @@
#include "MGLRandomHostObject.h"
#include "MGLSignInstaller.h"
#include "MGLVerifyInstaller.h"
#include "MGLWebCrypto.h"
#endif

namespace margelo {
Expand Down Expand Up @@ -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
5 changes: 5 additions & 0 deletions cpp/Random/MGLRandomHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 25 additions & 8 deletions cpp/Utils/MGLUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<void>(), 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<void>(), bptr->data, bptr->length);
return std::move(out).release();
}

// ByteSource ByteSource::FromEncodedString(Environment* env,
// Local<String> key,
Expand Down
Loading

0 comments on commit 430ed77

Please sign in to comment.