Skip to content
/ node Public
forked from nodejs/node

Commit

Permalink
crypto: add KeyObject.from and keyObject.export JWK format support
Browse files Browse the repository at this point in the history
  • Loading branch information
panva committed Nov 20, 2020
1 parent a44d285 commit e8259e8
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 16 deletions.
6 changes: 4 additions & 2 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1358,20 +1358,22 @@ format.

For public keys, the following encoding options can be used:

* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`.
* `type`: {string} Must be one of `'pkcs1'` (RSA only) or `'spki'`.
* `format`: {string} Must be `'pem'` or `'der'`.

For private keys, the following encoding options can be used:

* `format`: {string} Must be `'pem'`, `'der'`, or `'jwk'`.
* `type`: {string} Must be one of `'pkcs1'` (RSA only), `'pkcs8'` or
`'sec1'` (EC only).
* `format`: {string} Must be `'pem'` or `'der'`.
* `cipher`: {string} If specified, the private key will be encrypted with
the given `cipher` and `passphrase` using PKCS#5 v2.0 password based
encryption.
* `passphrase`: {string | Buffer} The passphrase to use for encryption, see
`cipher`.

When JWK format was selected, all other options are ignored.

When PEM encoding was selected, the result will be a string, otherwise it will
be a buffer containing the data encoded as DER.

Expand Down
73 changes: 73 additions & 0 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ const [
throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key);
return key[kKeyObject];
}

// TODO: Can we repurpose KeyObject.from? I don't see it neither
// used or documented.
static fromJwk(jwk) {
// TODO: validate jwk?.kty && typeof jwk.kty === 'string'
if (typeof jwk?.kty !== 'string') {
throw new TypeError('TODO')
}

const handle = new KeyObjectHandle();
const type = handle.initJwk(jwk);

switch (type) {
case kKeyTypeSecret:
return new SecretKeyObject(handle)
case kKeyTypePublic:
return new PublicKeyObject(handle)
case kKeyTypePrivate:
return new PrivateKeyObject(handle)
}
}
}

class SecretKeyObject extends KeyObject {
Expand Down Expand Up @@ -143,6 +164,38 @@ const [
return details;
}

function mapEcCrv(keyObject) {
switch (keyObject.asymmetricKeyDetails.namedCurve) {
case 'prime256v1':
return 'P-256'
case 'secp256k1':
return 'secp256k1'
case 'secp384r1':
return 'P-384'
case 'secp521r1':
return 'P-521'
default:
throw new TypeError('Unsupported JWK EC curve value');
}
}

function mapOkpCrv(keyObject) {
switch (keyObject.asymmetricKeyType) {
case 'ed25519':
return 'Ed25519'
case 'ed448':
return 'Ed448'
case 'x25519':
return 'X25519'
case 'x448':
return 'X448'
default:
throw new TypeError('Unsupported JWK OKP sub type value');
}
}

const jwkExport = Symbol('jwkExport');

class AsymmetricKeyObject extends KeyObject {
get asymmetricKeyType() {
return this[kAsymmetricKeyType] ||
Expand All @@ -155,6 +208,16 @@ const [
this[kHandle].keyDetail({})
));
}

[jwkExport]() {
const jwk = this[kHandle].exportJwk({});
if (jwk.kty === 'EC') {
jwk.crv = mapEcCrv(this)
} else if (jwk.kty === 'OKP') {
jwk.crv = mapOkpCrv(this)
}
return jwk;
}
}

class PublicKeyObject extends AsymmetricKeyObject {
Expand All @@ -167,6 +230,8 @@ const [
format,
type
} = parsePublicKeyEncoding(encoding, this.asymmetricKeyType);
if (format === 'jwk')
return this[jwkExport]();
return this[kHandle].export(format, type);
}
}
Expand All @@ -183,6 +248,8 @@ const [
cipher,
passphrase
} = parsePrivateKeyEncoding(encoding, this.asymmetricKeyType);
if (format === 'jwk')
return this[jwkExport]();
return this[kHandle].export(format, type, cipher, passphrase);
}
}
Expand All @@ -197,6 +264,8 @@ function parseKeyFormat(formatStr, defaultFormat, optionName) {
return kKeyFormatPEM;
else if (formatStr === 'der')
return kKeyFormatDER;
else if (formatStr === 'jwk')
return 'jwk';
throw new ERR_INVALID_ARG_VALUE(optionName, formatStr);
}

Expand Down Expand Up @@ -237,6 +306,10 @@ function parseKeyFormatAndType(enc, keyType, isPublic, objName) {
isInput ? kKeyFormatPEM : undefined,
option('format', objName));

if (format === 'jwk') {
return { format }
}

const type = parseKeyType(typeStr,
!isInput || format === kKeyFormatDER,
keyType,
Expand Down
6 changes: 3 additions & 3 deletions src/crypto/crypto_dsa.cc
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ std::shared_ptr<KeyObjectData> ImportJWKDsaKey(
!q_value->IsString() ||
!q_value->IsString() ||
(!x_value->IsUndefined() && !x_value->IsString())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -210,14 +210,14 @@ std::shared_ptr<KeyObjectData> ImportJWKDsaKey(
p.ToBN().release(),
q.ToBN().release(),
g.ToBN().release())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}

if (type == kKeyTypePrivate) {
ByteSource x = ByteSource::FromEncodedString(env, x_value.As<String>());
if (!DSA_set0_key(dsa.get(), nullptr, x.ToBN().release())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK DSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK DSA key");
return std::shared_ptr<KeyObjectData>();
}
}
Expand Down
60 changes: 56 additions & 4 deletions src/crypto/crypto_ecdh.cc
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,58 @@ WebCryptoKeyExportStatus ECKeyExportTraits::DoExport(
}
}

// TODO: this needs a new home
Maybe<bool> ExportJWKOkpKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Local<Object> target) {
ManagedEVPPKey pkey = key->GetAsymmetricKey();
// TODO: CHECK EVP_PKEY_id(pkey.get()) is one of
// EVP_PKEY_X448, EVP_PKEY_ED448, EVP_PKEY_X25519, EVP_PKEY_ED25519

size_t len = 0;
EVP_PKEY_get_raw_public_key(pkey.get(), nullptr, &len);

uint8_t* rawX = new uint8_t[len];
EVP_PKEY_get_raw_public_key(pkey.get(), rawX, &len);

if (target->Set(
env->context(),
env->jwk_kty_string(),
env->jwk_okp_string()).IsNothing()) {
return Nothing<bool>();
}

BignumPointer x(BN_new());
BN_bin2bn(rawX, len, x.get());

if (SetEncodedValue(
env,
target,
env->jwk_x_string(),
x.get(),
len).IsNothing()) {
return Nothing<bool>();
}

if (key->GetKeyType() == kKeyTypePrivate) {
uint8_t* rawD = new uint8_t[len];
EVP_PKEY_get_raw_private_key(pkey.get(), rawD, &len);

BignumPointer d(BN_new());
BN_bin2bn(rawD, len, d.get());

return SetEncodedValue(
env,
target,
env->jwk_d_string(),
d.get(),
len);
}

return Just(true);
}

Maybe<bool> ExportJWKEcKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Expand Down Expand Up @@ -680,15 +732,15 @@ std::shared_ptr<KeyObjectData> ImportJWKEcKey(
if (!x_value->IsString() ||
!y_value->IsString() ||
(!d_value->IsUndefined() && !d_value->IsString())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

KeyType type = d_value->IsString() ? kKeyTypePrivate : kKeyTypePublic;

ECKeyPointer ec(EC_KEY_new_by_curve_name(nid));
if (!ec) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -699,14 +751,14 @@ std::shared_ptr<KeyObjectData> ImportJWKEcKey(
ec.get(),
x.ToBN().get(),
y.ToBN().get())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}

if (type == kKeyTypePrivate) {
ByteSource d = ByteSource::FromEncodedString(env, d_value.As<String>());
if (!EC_KEY_set_private_key(ec.get(), d.ToBN().get())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK EC key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK EC key");
return std::shared_ptr<KeyObjectData>();
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/crypto/crypto_ecdh.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ struct ECKeyExportTraits final {

using ECKeyExportJob = KeyExportJob<ECKeyExportTraits>;

v8::Maybe<bool> ExportJWKOkpKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
v8::Local<v8::Object> target);

v8::Maybe<bool> ExportJWKEcKey(
Environment* env,
std::shared_ptr<KeyObjectData> key,
Expand Down
7 changes: 7 additions & 0 deletions src/crypto/crypto_keys.cc
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,13 @@ Maybe<bool> ExportJWKAsymmetricKey(
case EVP_PKEY_RSA_PSS: return ExportJWKRsaKey(env, key, target);
case EVP_PKEY_DSA: return ExportJWKDsaKey(env, key, target);
case EVP_PKEY_EC: return ExportJWKEcKey(env, key, target);
case EVP_PKEY_X448:
// Fall through
case EVP_PKEY_ED448:
// Fall through
case EVP_PKEY_X25519:
// Fall through
case EVP_PKEY_ED25519: return ExportJWKOkpKey(env, key, target);
}
THROW_ERR_CRYPTO_INVALID_KEYTYPE(env);
return Just(false);
Expand Down
13 changes: 6 additions & 7 deletions src/crypto/crypto_rsa.cc
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,12 @@ std::shared_ptr<KeyObjectData> ImportJWKRsaKey(
!jwk->Get(env->context(), env->jwk_d_string()).ToLocal(&d_value) ||
!n_value->IsString() ||
!e_value->IsString()) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

if (!d_value->IsUndefined() && !d_value->IsString()) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -443,7 +443,7 @@ std::shared_ptr<KeyObjectData> ImportJWKRsaKey(
n.ToBN().release(),
e.ToBN().release(),
nullptr)) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -459,7 +459,7 @@ std::shared_ptr<KeyObjectData> ImportJWKRsaKey(
!jwk->Get(env->context(), env->jwk_dp_string()).ToLocal(&dp_value) ||
!jwk->Get(env->context(), env->jwk_dq_string()).ToLocal(&dq_value) ||
!jwk->Get(env->context(), env->jwk_qi_string()).ToLocal(&qi_value)) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -468,7 +468,7 @@ std::shared_ptr<KeyObjectData> ImportJWKRsaKey(
!dp_value->IsString() ||
!dq_value->IsString() ||
!qi_value->IsString()) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}

Expand All @@ -486,7 +486,7 @@ std::shared_ptr<KeyObjectData> ImportJWKRsaKey(
dp.ToBN().release(),
dq.ToBN().release(),
qi.ToBN().release())) {
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JSK RSA key");
THROW_ERR_CRYPTO_INVALID_JWK(env, "Invalid JWK RSA key");
return std::shared_ptr<KeyObjectData>();
}
}
Expand Down Expand Up @@ -547,4 +547,3 @@ void Initialize(Environment* env, Local<Object> target) {
} // namespace RSAAlg
} // namespace crypto
} // namespace node

1 change: 1 addition & 0 deletions src/env.h
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ constexpr size_t kFsStatsBufferLength =
V(jwk_dsa_string, "DSA") \
V(jwk_e_string, "e") \
V(jwk_ec_string, "EC") \
V(jwk_okp_string, "OKP") \
V(jwk_g_string, "g") \
V(jwk_k_string, "k") \
V(jwk_p_string, "p") \
Expand Down

0 comments on commit e8259e8

Please sign in to comment.