Skip to content

Commit

Permalink
subtle (importKey,exportKey) (raw,jwk) (aes,ec,rsa)
Browse files Browse the repository at this point in the history
  • Loading branch information
boorad committed Mar 26, 2024
1 parent 3700146 commit fbaa4ac
Show file tree
Hide file tree
Showing 43 changed files with 3,025 additions and 669 deletions.
23 changes: 1 addition & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,34 +65,13 @@ expo prebuild

If you are using a library that depends on `crypto`, instead of polyfilling it with `crypto-browserify` (or `react-native-crypto`) you can use `react-native-quick-crypto` for a fully native implementation. This way you can get much faster crypto operations with just a single-line change!

### Using metro config

Use the [`resolveRequest`](https://facebook.github.io/metro/docs/resolution#resolverequest-customresolver) configuration option in your `metro.config.js`

```
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (moduleName === 'crypto') {
// when importing crypto, resolve to react-native-quick-crypto
return context.resolveRequest(
context,
'react-native-quick-crypto',
platform,
)
}
// otherwise chain to the standard Metro resolver.
return context.resolveRequest(context, moduleName, platform)
}
```

### Using babel-plugin-module-resolver

You need to install `babel-plugin-module-resolver`, it's a babel plugin that will alias any imports in the code with the values you pass to it. It tricks any module that will try to import certain dependencies with the native versions we require for React Native.

```sh
yarn add --dev babel-plugin-module-resolver
```

Then, in your `babel.config.js`, add the plugin to swap the `crypto`, `stream` and `buffer` dependencies:
In your `babel.config.js`, add the plugin to swap the `crypto`, `stream` and `buffer` dependencies:

```diff
module.exports = {
Expand Down
6 changes: 4 additions & 2 deletions cpp/Cipher/MGLCipherHostObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ void MGLCipherHostObject::installMethods() {
plaintext_len =
(int)args.getProperty(runtime, "plaintextLength").asNumber();
} else {
throw new jsi::JSError(runtime,
"plaintextLength property needs to be a number");
throw jsi::JSError(runtime,
"plaintextLength property needs to be a number");
}
}

Expand Down Expand Up @@ -577,6 +577,8 @@ bool MGLCipherHostObject::InitAuthenticated(const char *cipher_type, int iv_len,
// TODO(tniessen) Support CCM decryption in FIPS mode

#if OPENSSL_VERSION_MAJOR >= 3
// TODO: not sure where kind_ comes from in next line, but as we bump
// OpenSSL version we will need to look at Node.js code and figure it out.
if (mode == EVP_CIPH_CCM_MODE && kind_ == kDecipher &&
EVP_default_properties_is_fips_enabled(nullptr)) {
#else
Expand Down
2 changes: 1 addition & 1 deletion cpp/Cipher/MGLPublicCipher.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ std::optional<jsi::Value> MGLPublicCipher::Cipher(jsi::Runtime& runtime,
void* label = OPENSSL_memdup(oaep_label_buffer.data(runtime),
oaep_label_buffer.size(runtime));
if (label == nullptr) {
throw new jsi::JSError(runtime, "Error openSSL memdump oaep label");
throw jsi::JSError(runtime, "Error openSSL memdump oaep label");
}

if (0 >= EVP_PKEY_CTX_set0_rsa_oaep_label(
Expand Down
12 changes: 6 additions & 6 deletions cpp/Cipher/MGLPublicCipherInstaller.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,18 @@ FieldDefinition getPublicCipherFieldDefinition(
runtime, arguments, &offset);

if (!pkey) {
throw new jsi::JSError(runtime, "Could not generate key");
throw jsi::JSError(runtime, "Could not generate key");
}

auto buf = arguments[offset].asObject(runtime).getArrayBuffer(runtime);
if (!CheckSizeInt32(runtime, buf)) {
throw new jsi::JSError(runtime, "Data buffer is too long");
throw jsi::JSError(runtime, "Data buffer is too long");
}

uint32_t padding =
static_cast<uint32_t>(arguments[offset + 1].getNumber());
if (!padding) {
throw new jsi::JSError(runtime, "Invalid padding");
throw jsi::JSError(runtime, "Invalid padding");
}

const EVP_MD* digest = nullptr;
Expand All @@ -78,15 +78,15 @@ FieldDefinition getPublicCipherFieldDefinition(

digest = EVP_get_digestbyname(oaep_str.c_str());
if (digest == nullptr) {
throw new jsi::JSError(runtime, "Invalid digest (oaep_str)");
throw jsi::JSError(runtime, "Invalid digest (oaep_str)");
}
}

if (!arguments[offset + 3].isUndefined()) {
auto oaep_label_buffer =
arguments[offset + 3].getObject(runtime).getArrayBuffer(runtime);
if (!CheckSizeInt32(runtime, oaep_label_buffer)) {
throw new jsi::JSError(runtime, "oaep_label buffer is too long");
throw jsi::JSError(runtime, "oaep_label buffer is too long");
}
}

Expand All @@ -96,7 +96,7 @@ FieldDefinition getPublicCipherFieldDefinition(
runtime, pkey, padding, digest, arguments[offset + 3], buf);

if (!out.has_value()) {
throw new jsi::JSError(runtime, "Failed to decrypt");
throw jsi::JSError(runtime, "Failed to decrypt");
}

return out.value().getObject(runtime);
Expand Down
226 changes: 219 additions & 7 deletions cpp/Cipher/MGLRsa.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
//

#include "MGLRsa.h"
#ifdef ANDROID
#include "JSIUtils/MGLJSIMacros.h"
#else
#include "MGLJSIMacros.h"
#endif

#include <string>
#include <utility>

namespace margelo {
Expand Down Expand Up @@ -108,7 +114,7 @@ RsaKeyPairGenConfig prepareRsaKeyGenConfig(jsi::Runtime& runtime,
arguments[offset].asString(runtime).utf8(runtime).c_str());

if (config.md == nullptr) {
throw new jsi::JSError(runtime, "invalid digest");
throw jsi::JSError(runtime, "invalid digest");
}
}

Expand All @@ -118,7 +124,7 @@ RsaKeyPairGenConfig prepareRsaKeyGenConfig(jsi::Runtime& runtime,
arguments[offset + 1].asString(runtime).utf8(runtime).c_str());

if (config.mgf1_md == nullptr) {
throw new jsi::JSError(runtime, "invalid digest");
throw jsi::JSError(runtime, "invalid digest");
}
}

Expand All @@ -127,7 +133,7 @@ RsaKeyPairGenConfig prepareRsaKeyGenConfig(jsi::Runtime& runtime,
config.saltlen = static_cast<int>(arguments[offset + 2].asNumber());

if (config.saltlen < 0) {
throw new jsi::JSError(runtime, "salt length is out of range");
throw jsi::JSError(runtime, "salt length is out of range");
}
}

Expand All @@ -154,13 +160,13 @@ std::pair<JSVariant, JSVariant> generateRSAKeyPair(
EVPKeyCtxPointer ctx = setup(config);

if (!ctx) {
throw new jsi::JSError(runtime, "Error on key generation job");
throw jsi::JSError(runtime, "Error on key generation job");
}

// Generate the key
EVP_PKEY* pkey = nullptr;
if (!EVP_PKEY_keygen(ctx.get(), &pkey)) {
throw new jsi::JSError(runtime, "Error generating key");
throw jsi::JSError(runtime, "Error generating key");
}

config->key = ManagedEVPPKey(EVPKeyPointer(pkey));
Expand All @@ -173,11 +179,217 @@ std::pair<JSVariant, JSVariant> generateRSAKeyPair(
config->private_key_encoding);

if (!publicBuffer.has_value() || !privateBuffer.has_value()) {
throw jsi::JSError(runtime,
"Failed to encode public and/or private key");
throw jsi::JSError(runtime, "Failed to encode public and/or private key");
}

return {std::move(publicBuffer.value()), std::move(privateBuffer.value())};
}

jsi::Value ExportJWKRsaKey(jsi::Runtime &rt,
std::shared_ptr<KeyObjectData> key,
jsi::Object &target) {
ManagedEVPPKey m_pkey = key->GetAsymmetricKey();
// std::scoped_lock lock(*m_pkey.mutex()); // TODO: mutex/lock required?
int type = EVP_PKEY_id(m_pkey.get());
CHECK(type == EVP_PKEY_RSA || type == EVP_PKEY_RSA_PSS);

// TODO(tniessen): Remove the "else" branch once we drop support for OpenSSL
// versions older than 1.1.1e via FIPS / dynamic linking.
const RSA* rsa;
if (OpenSSL_version_num() >= 0x1010105fL) {
rsa = EVP_PKEY_get0_RSA(m_pkey.get());
} else {
rsa = static_cast<const RSA*>(EVP_PKEY_get0(m_pkey.get()));
}
CHECK_NOT_NULL(rsa);

const BIGNUM* n;
const BIGNUM* e;
const BIGNUM* d;
const BIGNUM* p;
const BIGNUM* q;
const BIGNUM* dp;
const BIGNUM* dq;
const BIGNUM* qi;
RSA_get0_key(rsa, &n, &e, &d);

target.setProperty(rt, "kty", "RSA");
target.setProperty(rt, "n", EncodeBignum(n, 0, true));
target.setProperty(rt, "e", EncodeBignum(e, 0, true));

if (key->GetKeyType() == kKeyTypePrivate) {
RSA_get0_factors(rsa, &p, &q);
RSA_get0_crt_params(rsa, &dp, &dq, &qi);
target.setProperty(rt, "d", EncodeBignum(d, 0, true));
target.setProperty(rt, "p", EncodeBignum(p, 0, true));
target.setProperty(rt, "q", EncodeBignum(q, 0, true));
target.setProperty(rt, "dp", EncodeBignum(dp, 0, true));
target.setProperty(rt, "dq", EncodeBignum(dq, 0, true));
target.setProperty(rt, "qi", EncodeBignum(qi, 0, true));
}

return std::move(target);
}

std::shared_ptr<KeyObjectData> ImportJWKRsaKey(jsi::Runtime &rt,
jsi::Object &jwk) {
jsi::Value n_value = jwk.getProperty(rt, "n");
jsi::Value e_value = jwk.getProperty(rt, "e");
jsi::Value d_value = jwk.getProperty(rt, "d");

if (!n_value.isString() ||
!e_value.isString()) {
throw jsi::JSError(rt, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

if (!d_value.isUndefined() && !d_value.isString()) {
throw jsi::JSError(rt, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

KeyType type = d_value.isString() ? kKeyTypePrivate : kKeyTypePublic;

RsaPointer rsa(RSA_new());

ByteSource n = ByteSource::FromEncodedString(rt, n_value.asString(rt).utf8(rt));
ByteSource e = ByteSource::FromEncodedString(rt, e_value.asString(rt).utf8(rt));

if (!RSA_set0_key(
rsa.get(),
n.ToBN().release(),
e.ToBN().release(),
nullptr)) {
throw jsi::JSError(rt, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

if (type == kKeyTypePrivate) {
jsi::Value p_value = jwk.getProperty(rt, "p");
jsi::Value q_value = jwk.getProperty(rt, "q");
jsi::Value dp_value = jwk.getProperty(rt, "dp");
jsi::Value dq_value = jwk.getProperty(rt, "dq");
jsi::Value qi_value = jwk.getProperty(rt, "qi");

if (!p_value.isString() ||
!q_value.isString() ||
!dp_value.isString() ||
!dq_value.isString() ||
!qi_value.isString()) {
throw jsi::JSError(rt, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

ByteSource d = ByteSource::FromEncodedString(rt, d_value.asString(rt).utf8(rt));
ByteSource q = ByteSource::FromEncodedString(rt, q_value.asString(rt).utf8(rt));
ByteSource p = ByteSource::FromEncodedString(rt, p_value.asString(rt).utf8(rt));
ByteSource dp = ByteSource::FromEncodedString(rt, dp_value.asString(rt).utf8(rt));
ByteSource dq = ByteSource::FromEncodedString(rt, dq_value.asString(rt).utf8(rt));
ByteSource qi = ByteSource::FromEncodedString(rt, qi_value.asString(rt).utf8(rt));

if (!RSA_set0_key(rsa.get(), nullptr, nullptr, d.ToBN().release()) ||
!RSA_set0_factors(rsa.get(), p.ToBN().release(), q.ToBN().release()) ||
!RSA_set0_crt_params(
rsa.get(),
dp.ToBN().release(),
dq.ToBN().release(),
qi.ToBN().release())) {
throw jsi::JSError(rt, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}
}

EVPKeyPointer pkey(EVP_PKEY_new());
CHECK_EQ(EVP_PKEY_set1_RSA(pkey.get(), rsa.get()), 1);

return KeyObjectData::CreateAsymmetric(type, ManagedEVPPKey(std::move(pkey)));
}

jsi::Value GetRsaKeyDetail(jsi::Runtime &rt,
std::shared_ptr<KeyObjectData> key) {
jsi::Object target = jsi::Object(rt);
const BIGNUM* e; // Public Exponent
const BIGNUM* n; // Modulus

ManagedEVPPKey m_pkey = key->GetAsymmetricKey();
// std::scoped_lock lock(*m_pkey.mutex()); // TODO: mutex/lock required?
int type = EVP_PKEY_id(m_pkey.get());
CHECK(type == EVP_PKEY_RSA || type == EVP_PKEY_RSA_PSS);

// TODO(tniessen): Remove the "else" branch once we drop support for OpenSSL
// versions older than 1.1.1e via FIPS / dynamic linking.
const RSA* rsa;
if (OpenSSL_version_num() >= 0x1010105fL) {
rsa = EVP_PKEY_get0_RSA(m_pkey.get());
} else {
rsa = static_cast<const RSA*>(EVP_PKEY_get0(m_pkey.get()));
}
CHECK_NOT_NULL(rsa);

RSA_get0_key(rsa, &n, &e, nullptr);

size_t modulus_length = BN_num_bits(n);
// TODO: should this be modulusLength or n?
target.setProperty(rt, "modulusLength", static_cast<double>(modulus_length));

size_t exp_size = BN_num_bytes(e);
// TODO: should this be publicExponent or e?
target.setProperty(rt, "publicExponent", EncodeBignum(e, exp_size, true));

if (type == EVP_PKEY_RSA_PSS) {
// Due to the way ASN.1 encoding works, default values are omitted when
// encoding the data structure. However, there are also RSA-PSS keys for
// which no parameters are set. In that case, the ASN.1 RSASSA-PSS-params
// sequence will be missing entirely and RSA_get0_pss_params will return
// nullptr. If parameters are present but all parameters are set to their
// default values, an empty sequence will be stored in the ASN.1 structure.
// In that case, RSA_get0_pss_params does not return nullptr but all fields
// of the returned RSA_PSS_PARAMS will be set to nullptr.

const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa);
if (params != nullptr) {
int hash_nid = NID_sha1;
int mgf_nid = NID_mgf1;
int mgf1_hash_nid = NID_sha1;
int64_t salt_length = 20;

if (params->hashAlgorithm != nullptr) {
const ASN1_OBJECT* hash_obj;
X509_ALGOR_get0(&hash_obj, nullptr, nullptr, params->hashAlgorithm);
hash_nid = OBJ_obj2nid(hash_obj);
}

target.setProperty(rt, "hashAlgorithm", std::string(OBJ_nid2ln(hash_nid)));

if (params->maskGenAlgorithm != nullptr) {
const ASN1_OBJECT* mgf_obj;
X509_ALGOR_get0(&mgf_obj, nullptr, nullptr, params->maskGenAlgorithm);
mgf_nid = OBJ_obj2nid(mgf_obj);
if (mgf_nid == NID_mgf1) {
const ASN1_OBJECT* mgf1_hash_obj;
X509_ALGOR_get0(&mgf1_hash_obj, nullptr, nullptr, params->maskHash);
mgf1_hash_nid = OBJ_obj2nid(mgf1_hash_obj);
}
}

// If, for some reason, the MGF is not MGF1, then the MGF1 hash function
// is intentionally not added to the object.
if (mgf_nid == NID_mgf1) {
target.setProperty(rt, "mgf1HashAlgorithm", std::string(OBJ_nid2ln(mgf1_hash_nid)));
}

if (params->saltLength != nullptr) {
if (ASN1_INTEGER_get_int64(&salt_length, params->saltLength) != 1) {
throw jsi::JSError(rt, "ASN1_INTEGER_get_in64 error: " + ERR_get_error());
return target;
}
}

target.setProperty(rt, "saltLength", static_cast<double>(salt_length));
}
}

return target;
}

} // namespace margelo
Loading

0 comments on commit fbaa4ac

Please sign in to comment.