diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 39b380b99b..ed6713eed8 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -232,6 +232,7 @@ deno_core::extension!(deno_node, ops::crypto::op_node_verify, ops::crypto::op_node_verify_ed25519, ops::crypto::keys::op_node_create_private_key, + ops::crypto::keys::op_node_create_ed_raw, ops::crypto::keys::op_node_create_public_key, ops::crypto::keys::op_node_create_secret_key, ops::crypto::keys::op_node_derive_public_key_from_private_key, diff --git a/ext/node/ops/crypto/keys.rs b/ext/node/ops/crypto/keys.rs index 45849cbd93..7d7ec140e7 100644 --- a/ext/node/ops/crypto/keys.rs +++ b/ext/node/ops/crypto/keys.rs @@ -571,6 +571,50 @@ impl KeyObjectHandle { Ok(KeyObjectHandle::AsymmetricPublic(key)) } + pub fn new_ed_raw( + curve: &str, + data: &[u8], + is_public: bool, + ) -> Result { + match curve { + "Ed25519" => { + let data = data + .try_into() + .map_err(|_| type_error("invalid Ed25519 key"))?; + if !is_public { + Ok(KeyObjectHandle::AsymmetricPrivate( + AsymmetricPrivateKey::Ed25519( + ed25519_dalek::SigningKey::from_bytes(data), + ), + )) + } else { + Ok(KeyObjectHandle::AsymmetricPublic( + AsymmetricPublicKey::Ed25519( + ed25519_dalek::VerifyingKey::from_bytes(data)?, + ), + )) + } + } + "X25519" => { + let data: [u8; 32] = data + .try_into() + .map_err(|_| type_error("invalid x25519 key"))?; + if !is_public { + Ok(KeyObjectHandle::AsymmetricPrivate( + AsymmetricPrivateKey::X25519(x25519_dalek::StaticSecret::from( + data, + )), + )) + } else { + Ok(KeyObjectHandle::AsymmetricPublic( + AsymmetricPublicKey::X25519(x25519_dalek::PublicKey::from(data)), + )) + } + } + _ => Err(type_error("unsupported curve")), + } + } + pub fn new_asymmetric_public_key_from_js( key: &[u8], format: &str, @@ -1027,6 +1071,16 @@ pub fn op_node_create_private_key( ) } +#[op2] +#[cppgc] +pub fn op_node_create_ed_raw( + #[string] curve: &str, + #[buffer] key: &[u8], + is_public: bool, +) -> Result { + KeyObjectHandle::new_ed_raw(curve, key, is_public) +} + #[op2] #[cppgc] pub fn op_node_create_public_key( diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index 62cec47d67..97e565023a 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -12,6 +12,7 @@ const { } = primordials; import { + op_node_create_ed_raw, op_node_create_private_key, op_node_create_public_key, op_node_create_secret_key, @@ -32,6 +33,7 @@ import { kHandle } from "ext:deno_node/internal/crypto/constants.ts"; import { isStringOrBuffer } from "ext:deno_node/internal/crypto/cipher.ts"; import { ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, + ERR_CRYPTO_INVALID_JWK, ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, @@ -56,6 +58,7 @@ import { import { validateObject, validateOneOf, + validateString, } from "ext:deno_node/internal/validators.mjs"; import { BufferEncoding } from "ext:deno_node/_global.d.ts"; @@ -256,6 +259,64 @@ export function getKeyObjectHandle(key: KeyObject, ctx: KeyHandleContext) { return key[kHandle]; } +function getKeyObjectHandleFromJwk(key, ctx) { + validateObject(key, "key"); + validateOneOf( + key.kty, + "key.kty", + ["RSA", "EC", "OKP"], + ); + const isPublic = ctx === kConsumePublic || ctx === kCreatePublic; + + if (key.kty === "OKP") { + validateString(key.crv, "key.crv"); + validateOneOf( + key.crv, + "key.crv", + ["Ed25519", "Ed448", "X25519", "X448"], + ); + validateString(key.x, "key.x"); + + if (!isPublic) { + validateString(key.d, "key.d"); + } + + let keyData; + if (isPublic) { + keyData = Buffer.from(key.x, "base64"); + } else { + keyData = Buffer.from(key.d, "base64"); + } + + switch (key.crv) { + case "Ed25519": + case "X25519": + if (keyData.byteLength !== 32) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + case "Ed448": + if (keyData.byteLength !== 57) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + case "X448": + if (keyData.byteLength !== 56) { + throw new ERR_CRYPTO_INVALID_JWK(); + } + break; + } + + return op_node_create_ed_raw(key.crv, keyData, isPublic); + } + + if (key.kty === "EC") { + throw new TypeError("ec jwk imports not implemented"); + } + + throw new TypeError("rsa jwk imports not implemented"); +} + export function prepareAsymmetricKey( key: | string @@ -306,7 +367,12 @@ export function prepareAsymmetricKey( } else if (isCryptoKey(data)) { notImplemented("using CryptoKey as input"); } else if (isJwk(data) && format === "jwk") { - notImplemented("using JWK as input"); + return { + // @ts-ignore __proto__ is magic + __proto__: null, + handle: getKeyObjectHandleFromJwk(data, ctx), + format, + }; } // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index 9ec9f99491..9e0905ef4f 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -927,6 +927,12 @@ export class ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE extends NodeTypeError { } } +export class ERR_CRYPTO_INVALID_JWK extends NodeError { + constructor() { + super("ERR_CRYPTO_INVALID_JWK", "Invalid JWK"); + } +} + export class ERR_CRYPTO_INVALID_STATE extends NodeError { constructor(x: string) { super("ERR_CRYPTO_INVALID_STATE", `Invalid state for operation ${x}`); @@ -2733,6 +2739,7 @@ export default { ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, ERR_CRYPTO_INVALID_DIGEST, ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, + ERR_CRYPTO_INVALID_JWK, ERR_CRYPTO_INVALID_STATE, ERR_CRYPTO_PBKDF2_ERROR, ERR_CRYPTO_SCRYPT_INVALID_PARAMETER, diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index 6365119e23..6c2c9f8519 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -439,3 +439,61 @@ Deno.test("create private key with invalid utf-8 string", function () { "not valid utf8", ); }); + +Deno.test("Ed25519 jwk public key #1", function () { + const key = { + "kty": "OKP", + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + }; + const keyObject = createPublicKey({ key, format: "jwk" }); + + assertEquals(keyObject.type, "public"); + const spkiActual = keyObject.export({ type: "spki", format: "pem" }); + + const spkiExpected = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo= +-----END PUBLIC KEY----- +`; + + assertEquals(spkiActual, spkiExpected); +}); + +Deno.test("Ed25519 jwk public key #2", function () { + const key = { + "kty": "OKP", + "crv": "Ed25519", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + }; + + const keyObject = createPublicKey({ key, format: "jwk" }); + assertEquals(keyObject.type, "public"); + + const spki = keyObject.export({ type: "spki", format: "pem" }); + const spkiExpected = `-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo= +-----END PUBLIC KEY----- +`; + assertEquals(spki, spkiExpected); +}); + +Deno.test("Ed25519 jwk private key", function () { + const key = { + "kty": "OKP", + "crv": "Ed25519", + "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo", + }; + + const keyObject = createPrivateKey({ key, format: "jwk" }); + assertEquals(keyObject.type, "private"); + + const pkcs8Actual = keyObject.export({ type: "pkcs8", format: "pem" }); + const pkcs8Expected = `-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g +-----END PRIVATE KEY----- +`; + + assertEquals(pkcs8Actual, pkcs8Expected); +});