Skip to content

Commit

Permalink
Implement Node.js crypto KeyObject asymmetricKeyDetails
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed May 16, 2023
1 parent 7318856 commit bcce1fd
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 48 deletions.
10 changes: 9 additions & 1 deletion src/node/internal/crypto_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ import {
default as cryptoImpl
} from 'node-internal:crypto';

import {
arrayBufferToUnsignedBigInt,
} from 'node-internal:crypto_util';

import {
isAnyArrayBuffer,
isArrayBuffer,
Expand Down Expand Up @@ -156,7 +160,11 @@ export abstract class KeyObject {

abstract class AsymmetricKeyObject extends KeyObject {
get asymmetricKeyDetails() : AsymmetricKeyDetails {
return cryptoImpl.getAsymmetricKeyDetail(this[kHandle]);
let detail = cryptoImpl.getAsymmetricKeyDetail(this[kHandle]);
if (isArrayBuffer(detail.publicExponent)) {
detail.publicExponent = arrayBufferToUnsignedBigInt(detail.publicExponent as any);
}
return detail;
}

get asymmetricKeyType() : AsymmetricKeyType {
Expand Down
30 changes: 4 additions & 26 deletions src/node/internal/crypto_random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ import {
kMaxLength
} from 'node-internal:internal_buffer';

import {
arrayBufferToUnsignedBigInt,
} from 'node-internal:crypto_util';

export type RandomBytesCallback = (err: any|null, buffer: Uint8Array) => void;
export function randomBytes(size: number, callback: RandomBytesCallback): void;
export function randomBytes(size: number): Uint8Array;
Expand Down Expand Up @@ -361,32 +365,6 @@ export function generatePrime(size: number,
}).then((val) => callback!(null, val), (err) => callback!(err));
}

/**
* 48 is the ASCII code for '0', 97 is the ASCII code for 'a'.
* @param {number} number An integer between 0 and 15.
* @returns {number} corresponding to the ASCII code of the hex representation
* of the parameter.
*/
const numberToHexCharCode = (number : number) => (number < 10 ? 48 : 87) + number;

/**
* @param {ArrayBuffer} buf An ArrayBuffer.
* @return {bigint}
*/
function arrayBufferToUnsignedBigInt(buf: ArrayBuffer) {
const length = buf.byteLength;
const chars = Array(length * 2);
const view = new DataView(buf);

for (let i = 0; i < length; i++) {
const val = view.getUint8(i);
chars[2 * i] = numberToHexCharCode(val >> 4);
chars[2 * i + 1] = numberToHexCharCode(val & 0xf);
}

return BigInt(`0x${String.fromCharCode.apply(null, chars)}`);
}

function unsignedBigIntToBuffer(bigint: bigint, name: string) {
if (bigint < 0) {
throw new ERR_OUT_OF_RANGE(name, '>= 0', bigint);
Expand Down
26 changes: 26 additions & 0 deletions src/node/internal/crypto_util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,29 @@ export function getArrayBufferOrView(buffer: Buffer | ArrayBuffer | ArrayBufferV
}
return buffer;
}

/**
* 48 is the ASCII code for '0', 97 is the ASCII code for 'a'.
* @param {number} number An integer between 0 and 15.
* @returns {number} corresponding to the ASCII code of the hex representation
* of the parameter.
*/
export const numberToHexCharCode = (number : number) => (number < 10 ? 48 : 87) + number;

/**
* @param {ArrayBuffer} buf An ArrayBuffer.
* @return {bigint}
*/
export function arrayBufferToUnsignedBigInt(buf: ArrayBuffer) {
const length = buf.byteLength;
const chars = Array(length * 2);
const view = new DataView(buf);

for (let i = 0; i < length; i++) {
const val = view.getUint8(i);
chars[2 * i] = numberToHexCharCode(val >> 4);
chars[2 * i + 1] = numberToHexCharCode(val & 0xf);
}

return BigInt(`0x${String.fromCharCode.apply(null, chars)}`);
}
86 changes: 86 additions & 0 deletions src/workerd/api/crypto-impl-asymmetric.c++
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,73 @@ private:
"\" in \"raw\" format.");
}

CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const override {
// Adapted from the Node.js implementation of GetRsaKeyDetail
const BIGNUM* e; // Public Exponent
const BIGNUM* n; // Modulus

int type = EVP_PKEY_id(getEvpPkey());
KJ_REQUIRE(type == EVP_PKEY_RSA || type == EVP_PKEY_RSA_PSS);

const RSA* rsa = EVP_PKEY_get0_RSA(getEvpPkey());
KJ_ASSERT(rsa != nullptr);
RSA_get0_key(rsa, &n, &e, nullptr);

CryptoKey::AsymmetricKeyDetails details;
details.modulusLength = BN_num_bits(n);

auto public_exponent = kj::heapArray<kj::byte>(BN_num_bytes(e));
KJ_ASSERT(BN_bn2binpad(e, static_cast<unsigned char*>(public_exponent.begin()),
public_exponent.size()) == public_exponent.size());
details.publicExponent = kj::mv(public_exponent);

// TODO(soon): Does BoringSSL not support retrieving RSA_PSS params?
// 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) {
// hash_nid = OBJ_obj2nid(params->hashAlgorithm->algorithm);
// }
// details.hashAlgorithm = kj::str(OBJ_nid2ln(hash_nid));

// if (params->maskGenAlgorithm != nullptr) {
// mgf_nid = OBJ_obj2nid(params->maskGenAlgorithm->algorithm);
// if (mgf_nid == NID_mgf1) {
// mgf1_hash_nid = OBJ_obj2nid(params->maskHash->algorithm);
// }
// }

// // 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) {
// details.mgf1HashAlgorithm = kj::str(OBJ_nid2ln(mgf1_hash_nid));
// }

// if (params->saltLength != nullptr) {
// JSG_REQUIRE(ASN1_INTEGER_get_int64(&salt_length, params->saltLength) == 1,
// Error, "Unable to get salt length from RSA-PSS parameters");
// }
// details.saltLength = static_cast<double>(salt_length);
// }
// }

return kj::mv(details);
}

virtual kj::String jwkHashAlgorithmName() const = 0;
};

Expand Down Expand Up @@ -1387,6 +1454,20 @@ private:
return kj::Array<kj::byte>(raw, raw_len, SslArrayDisposer::INSTANCE);
}

CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const override {
// Adapted from Node.js' GetEcKeyDetail
KJ_REQUIRE(EVP_PKEY_id(getEvpPkey()) == EVP_PKEY_EC);
const EC_KEY* ec = EVP_PKEY_get0_EC_KEY(getEvpPkey());
KJ_ASSERT(ec != nullptr);

const EC_GROUP* group = EC_KEY_get0_group(ec);
int nid = EC_GROUP_get_curve_name(group);

return CryptoKey::AsymmetricKeyDetails {
.namedCurve = kj::str(OBJ_nid2sn(nid))
};
}

CryptoKey::EllipticKeyAlgorithm keyAlgorithm;
uint rsSize;
};
Expand Down Expand Up @@ -1872,6 +1953,11 @@ public:
return sharedSecret.releaseAsArray();
}

CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const override {
// Node.js implementation for EdDsa keys currently does not provide any detail
return CryptoKey::AsymmetricKeyDetails {};
}

private:
SubtleCrypto::JsonWebKey exportJwk() const override final {
KJ_ASSERT(getAlgorithmName() == "X25519"_kj || getAlgorithmName() == "Ed25519"_kj ||
Expand Down
6 changes: 6 additions & 0 deletions src/workerd/api/crypto-impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ class CryptoKey::Impl {

virtual kj::StringPtr getAlgorithmName() const = 0;

virtual CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail() const {
JSG_FAIL_REQUIRE(DOMNotSupportedError,
"The getAsymmetricKeyDetail operation is not implemented for \"", getAlgorithmName(),
"\".");
}

// JS API implementation

virtual AlgorithmVariant getAlgorithm() const = 0;
Expand Down
4 changes: 4 additions & 0 deletions src/workerd/api/crypto.c++
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,10 @@ bool CryptoKey::operator==(const CryptoKey& other) const {
return this == &other || (getType() == other.getType() && impl->equals(*other.impl));
}

CryptoKey::AsymmetricKeyDetails CryptoKey::getAsymmetricKeyDetails() const {
return impl->getAsymmetricKeyDetail();
}

jsg::Promise<kj::Array<kj::byte>> SubtleCrypto::encrypt(
jsg::Lock& js,
kj::OneOf<kj::String, EncryptAlgorithm> algorithmParam,
Expand Down
22 changes: 22 additions & 0 deletions src/workerd/api/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,27 @@ class CryptoKey: public jsg::Object {
JSG_STRUCT(name, hash, namedCurve, length);
};

struct AsymmetricKeyDetails {
// Used as part of the Node.js crypto implementation of KeyObject.
// Defined here instead of api/node/crypto.h because it it is needed
// by CryptoKey::Impl to provide the actual implementation.
jsg::Optional<uint32_t> modulusLength;
jsg::Optional<kj::Array<kj::byte>> publicExponent;
jsg::Optional<kj::String> hashAlgorithm;
jsg::Optional<kj::String> mgf1HashAlgorithm;
jsg::Optional<double> saltLength;
jsg::Optional<uint32_t> divisorLength;
jsg::Optional<kj::String> namedCurve;
JSG_STRUCT(modulusLength,
publicExponent,
hashAlgorithm,
mgf1HashAlgorithm,
saltLength,
divisorLength,
namedCurve);
};
AsymmetricKeyDetails getAsymmetricKeyDetails() const;

~CryptoKey() noexcept(false);

kj::StringPtr getAlgorithmName() const;
Expand Down Expand Up @@ -683,6 +704,7 @@ class Crypto: public jsg::Object {
api::CryptoKey::RsaKeyAlgorithm, \
api::CryptoKey::EllipticKeyAlgorithm, \
api::CryptoKey::ArbitraryKeyAlgorithm, \
api::CryptoKey::AsymmetricKeyDetails, \
api::DigestStream

} // namespace workerd::api
5 changes: 3 additions & 2 deletions src/workerd/api/node/crypto-keys.c++
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,10 @@ bool CryptoImpl::equals(jsg::Lock& js, jsg::Ref<CryptoKey> key, jsg::Ref<CryptoK
return *key == *otherKey;
}

CryptoImpl::AsymmetricKeyDetails CryptoImpl::getAsymmetricKeyDetail(
CryptoKey::AsymmetricKeyDetails CryptoImpl::getAsymmetricKeyDetail(
jsg::Lock& js, jsg::Ref<CryptoKey> key) {
KJ_UNIMPLEMENTED("not implemented");
JSG_REQUIRE(key->getType() != "secret"_kj, Error, "Secret keys do not have asymmetric details");
return key->getAsymmetricKeyDetails();
}

kj::StringPtr CryptoImpl::getAsymmetricKeyType(jsg::Lock& js, jsg::Ref<CryptoKey> key) {
Expand Down
20 changes: 1 addition & 19 deletions src/workerd/api/node/crypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,6 @@ class CryptoImpl final: public jsg::Object {
JSG_STRUCT(type, format, cipher, passphrase);
};

struct AsymmetricKeyDetails {
jsg::Optional<uint32_t> modulusLength;
jsg::Optional<uint64_t> publicExponent;
jsg::Optional<kj::String> hashAlgorithm;
jsg::Optional<kj::String> mgf1HashAlgorithm;
jsg::Optional<uint32_t> saltLength;
jsg::Optional<uint32_t> divisorLength;
jsg::Optional<kj::String> namedCurve;
JSG_STRUCT(modulusLength,
publicExponent,
hashAlgorithm,
mgf1HashAlgorithm,
saltLength,
divisorLength,
namedCurve);
};

struct GenerateKeyPairOptions {
jsg::Optional<uint32_t> modulusLength;
jsg::Optional<uint64_t> publicExponent;
Expand Down Expand Up @@ -95,7 +78,7 @@ class CryptoImpl final: public jsg::Object {

bool equals(jsg::Lock& js, jsg::Ref<CryptoKey> key, jsg::Ref<CryptoKey> otherKey);

AsymmetricKeyDetails getAsymmetricKeyDetail(jsg::Lock& js, jsg::Ref<CryptoKey> key);
CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail(jsg::Lock& js, jsg::Ref<CryptoKey> key);
kj::StringPtr getAsymmetricKeyType(jsg::Lock& js, jsg::Ref<CryptoKey> key);

CryptoKeyPair generateKeyPair(jsg::Lock& js, kj::String type, GenerateKeyPairOptions options);
Expand Down Expand Up @@ -125,7 +108,6 @@ class CryptoImpl final: public jsg::Object {
#define EW_NODE_CRYPTO_ISOLATE_TYPES \
api::node::CryptoImpl, \
api::node::CryptoImpl::KeyExportOptions, \
api::node::CryptoImpl::AsymmetricKeyDetails, \
api::node::CryptoImpl::GenerateKeyPairOptions, \
api::node::CryptoImpl::CreateAsymmetricKeyOptions
} // namespace workerd::api::node
4 changes: 4 additions & 0 deletions src/workerd/api/node/crypto_keys-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ export const asymmetric_key_equals_test = {
const rsa_ko = KeyObject.from(rsa);
const rsa2_ko = KeyObject.from(rsa2);

strictEqual(rsa_ko.asymmetricKeyDetails.modulusLength, 2048);
strictEqual(rsa_ko.asymmetricKeyDetails.publicExponent, 65537n);
strictEqual(jwk1_ko.asymmetricKeyDetails.namedCurve, 'secp384r1');

ok(jwk1_ko.equals(jwk1_ko));
ok(jwk1_ko.equals(jwk2_ko));
ok(!rsa_ko.equals(jwk1_ko));
Expand Down

0 comments on commit bcce1fd

Please # to comment.