// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials import { primordials } from "ext:core/mod.js"; const { ObjectDefineProperties, SymbolToStringTag, } = primordials; import { op_node_create_ec_jwk, op_node_create_ed_raw, op_node_create_private_key, op_node_create_public_key, op_node_create_rsa_jwk, op_node_create_secret_key, op_node_derive_public_key_from_private_key, op_node_export_private_key_der, op_node_export_private_key_pem, op_node_export_public_key_der, op_node_export_public_key_jwk, op_node_export_public_key_pem, op_node_export_secret_key, op_node_export_secret_key_b64url, op_node_get_asymmetric_key_details, op_node_get_asymmetric_key_type, op_node_get_symmetric_key_size, op_node_key_type, } from "ext:core/ops"; 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, } from "ext:deno_node/internal/errors.ts"; import { notImplemented } from "ext:deno_node/_utils.ts"; import type { KeyFormat, PrivateKeyInput, PublicKeyInput, } from "ext:deno_node/internal/crypto/types.ts"; import { Buffer } from "node:buffer"; import { isAnyArrayBuffer, isArrayBufferView, } from "ext:deno_node/internal/util/types.ts"; import { hideStackFrames } from "ext:deno_node/internal/errors.ts"; import { isCryptoKey, isKeyObject, kKeyType, } from "ext:deno_node/internal/crypto/_keys.ts"; import { validateObject, validateOneOf, validateString, } from "ext:deno_node/internal/validators.mjs"; import { BufferEncoding } from "ext:deno_node/_global.d.ts"; export const getArrayBufferOrView = hideStackFrames( ( buffer: ArrayBufferView | ArrayBuffer | string | Buffer, name: string, encoding?: BufferEncoding | "buffer", ): | ArrayBuffer | SharedArrayBuffer | Buffer | DataView | BigInt64Array | BigUint64Array | Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint8ClampedArray | Uint16Array | Uint32Array => { if (isAnyArrayBuffer(buffer)) { return new Uint8Array(buffer); } if (typeof buffer === "string") { if (encoding === "buffer") { encoding = "utf8"; } return Buffer.from(buffer, encoding); } if (!isArrayBufferView(buffer)) { throw new ERR_INVALID_ARG_TYPE( name, [ "string", "ArrayBuffer", "Buffer", "TypedArray", "DataView", ], buffer, ); } return buffer; }, ); export interface AsymmetricKeyDetails { /** * Key size in bits (RSA, DSA). */ modulusLength?: number | undefined; /** * Public exponent (RSA). */ publicExponent?: bigint | undefined; /** * Name of the message digest (RSA-PSS). */ hashAlgorithm?: string | undefined; /** * Name of the message digest used by MGF1 (RSA-PSS). */ mgf1HashAlgorithm?: string | undefined; /** * Minimal salt length in bytes (RSA-PSS). */ saltLength?: number | undefined; /** * Size of q in bits (DSA). */ divisorLength?: number | undefined; /** * Name of the curve (EC). */ namedCurve?: string | undefined; } export type KeyObjectType = "secret" | "public" | "private"; export interface KeyExportOptions<T extends KeyFormat> { type: "pkcs1" | "spki" | "pkcs8" | "sec1"; format: T; cipher?: string | undefined; passphrase?: string | Buffer | undefined; } export interface JwkKeyExportOptions { format: "jwk"; } export enum KeyHandleContext { kConsumePublic = 0, kConsumePrivate = 1, kCreatePublic = 2, kCreatePrivate = 3, } export const kConsumePublic = KeyHandleContext.kConsumePublic; export const kConsumePrivate = KeyHandleContext.kConsumePrivate; export const kCreatePublic = KeyHandleContext.kCreatePublic; export const kCreatePrivate = KeyHandleContext.kCreatePrivate; function isJwk(obj: unknown): obj is { kty: unknown } { // @ts-ignore this is fine return typeof obj === "object" && obj != null && obj.kty !== undefined; } export type KeyObjectHandle = { ___keyObjectHandle: true }; export class KeyObject { [kKeyType]: KeyObjectType; [kHandle]: KeyObjectHandle; constructor(type: KeyObjectType, handle: KeyObjectHandle) { if (type !== "secret" && type !== "public" && type !== "private") { throw new ERR_INVALID_ARG_VALUE("type", type); } this[kKeyType] = type; this[kHandle] = handle; } get type(): KeyObjectType { return this[kKeyType]; } get symmetricKeySize(): number | undefined { notImplemented("crypto.KeyObject.prototype.symmetricKeySize"); return undefined; } static from(key: CryptoKey): KeyObject { if (!isCryptoKey(key)) { throw new ERR_INVALID_ARG_TYPE("key", "CryptoKey", key); } notImplemented("crypto.KeyObject.prototype.from"); } equals(otherKeyObject: KeyObject): boolean { if (!isKeyObject(otherKeyObject)) { throw new ERR_INVALID_ARG_TYPE( "otherKeyObject", "KeyObject", otherKeyObject, ); } notImplemented("crypto.KeyObject.prototype.equals"); } export(options: KeyExportOptions<"pem">): string | Buffer; export(options?: KeyExportOptions<"der">): Buffer; export(options?: JwkKeyExportOptions): JsonWebKey; export(_options?: unknown): string | Buffer | JsonWebKey { notImplemented("crypto.KeyObject.prototype.export"); } } ObjectDefineProperties(KeyObject.prototype, { [SymbolToStringTag]: { // @ts-expect-error __proto__ is magic __proto__: null, configurable: true, value: "KeyObject", }, }); export interface JsonWebKeyInput { key: JsonWebKey; format: "jwk"; } export function getKeyObjectHandle(key: KeyObject, ctx: KeyHandleContext) { if (ctx === kCreatePrivate) { throw new ERR_INVALID_ARG_TYPE( "key", ["string", "ArrayBuffer", "Buffer", "TypedArray", "DataView"], key, ); } if (key.type !== "private") { if (ctx === kConsumePrivate || ctx === kCreatePublic) { throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "private"); } if (key.type !== "public") { throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE( key.type, "private or public", ); } } 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") { validateString(key.crv, "key.crv"); validateString(key.x, "key.x"); validateString(key.y, "key.y"); if (!isPublic) { validateString(key.d, "key.d"); } return op_node_create_ec_jwk(key, isPublic); } // RSA validateString(key.n, "key.n"); validateString(key.e, "key.e"); const jwk = { kty: key.kty, n: key.n, e: key.e, }; if (!isPublic) { validateString(key.d, "key.d"); validateString(key.p, "key.p"); validateString(key.q, "key.q"); validateString(key.dp, "key.dp"); validateString(key.dq, "key.dq"); validateString(key.qi, "key.qi"); jwk.d = key.d; jwk.p = key.p; jwk.q = key.q; jwk.dp = key.dp; jwk.dq = key.dq; jwk.qi = key.qi; } return op_node_create_rsa_jwk(jwk, isPublic); } export function prepareAsymmetricKey( key: | string | ArrayBuffer | Buffer | ArrayBufferView | KeyObject | CryptoKey | PrivateKeyInput | PublicKeyInput | JsonWebKeyInput, ctx: KeyHandleContext, ): | { handle: KeyObjectHandle; format?: "jwk" } | { data: ArrayBuffer | ArrayBufferView; format: KeyFormat; type: "pkcs1" | "spki" | "pkcs8" | "sec1" | undefined; passphrase: Buffer | ArrayBuffer | ArrayBufferView | undefined; } { if (isKeyObject(key)) { // Best case: A key object, as simple as that. return { // @ts-ignore __proto__ is magic __proto__: null, handle: getKeyObjectHandle(key, ctx), }; } else if (isCryptoKey(key)) { notImplemented("using CryptoKey as input"); } else if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. return { // @ts-ignore __proto__ is magic __proto__: null, format: "pem", data: getArrayBufferOrView(key, "key"), }; } else if (typeof key === "object") { const { key: data, format } = key; // The 'key' property can be a KeyObject as well to allow specifying // additional options such as padding along with the key. if (isKeyObject(data)) { return { // @ts-ignore __proto__ is magic __proto__: null, handle: getKeyObjectHandle(data, ctx), }; } else if (isCryptoKey(data)) { notImplemented("using CryptoKey as input"); } else if (isJwk(data) && format === "jwk") { 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)) { throw new ERR_INVALID_ARG_TYPE( "key.key", getKeyTypes(ctx !== kCreatePrivate), data, ); } const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; return { data: getArrayBufferOrView( data, "key", (key as PrivateKeyInput | PublicKeyInput).encoding, ), ...parseKeyEncoding(key, undefined, isPublic), }; } throw new ERR_INVALID_ARG_TYPE( "key", getKeyTypes(ctx !== kCreatePrivate), key, ); } function parseKeyEncoding( enc: { cipher?: string; passphrase?: string | Buffer | ArrayBuffer | ArrayBufferView; encoding?: BufferEncoding | "buffer"; format?: string; type?: string; }, keyType: string | undefined, isPublic: boolean | undefined, objName?: string, ): { format: KeyFormat; type: "pkcs1" | "spki" | "pkcs8" | "sec1" | undefined; passphrase: Buffer | ArrayBuffer | ArrayBufferView | undefined; cipher: string | undefined; } { if (enc === null || typeof enc !== "object") { throw new ERR_INVALID_ARG_TYPE("options", "object", enc); } const isInput = keyType === undefined; const { format, type, } = parseKeyFormatAndType(enc, keyType, isPublic, objName); let cipher, passphrase, encoding; if (isPublic !== true) { ({ cipher, passphrase, encoding } = enc); if (!isInput) { if (cipher != null) { if (typeof cipher !== "string") { throw new ERR_INVALID_ARG_VALUE(option("cipher", objName), cipher); } if ( format === "der" && (type === "pkcs1" || type === "sec1") ) { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( type, "does not support encryption", ); } } else if (passphrase !== undefined) { throw new ERR_INVALID_ARG_VALUE(option("cipher", objName), cipher); } } if ( (isInput && passphrase !== undefined && !isStringOrBuffer(passphrase)) || (!isInput && cipher != null && !isStringOrBuffer(passphrase)) ) { throw new ERR_INVALID_ARG_VALUE( option("passphrase", objName), passphrase, ); } } if (passphrase !== undefined) { passphrase = getArrayBufferOrView(passphrase, "key.passphrase", encoding); } return { // @ts-ignore __proto__ is magic __proto__: null, format, type, cipher, passphrase, }; } function option(name: string, objName?: string) { return objName === undefined ? `options.${name}` : `options.${objName}.${name}`; } function parseKeyFormatAndType( enc: { format?: string; type?: string }, keyType: string | undefined, isPublic: boolean | undefined, objName?: string, ): { format: KeyFormat; type: "pkcs1" | "spki" | "pkcs8" | "sec1" | undefined; } { const { format: formatStr, type: typeStr } = enc; const isInput = keyType === undefined; const format = parseKeyFormat( formatStr, isInput ? "pem" : undefined, option("format", objName), ); const type = parseKeyType( typeStr, !isInput || format === "der", keyType, isPublic, option("type", objName), ); return { // @ts-ignore __proto__ is magic __proto__: null, format, type, }; } function parseKeyFormat( formatStr: string | undefined, defaultFormat: KeyFormat | undefined, optionName: string, ): KeyFormat { if (formatStr === undefined && defaultFormat !== undefined) { return defaultFormat; } else if (formatStr === "pem") { return "pem"; } else if (formatStr === "der") { return "der"; } throw new ERR_INVALID_ARG_VALUE(optionName, formatStr); } function parseKeyType( typeStr: string | undefined, required: boolean, keyType: string | undefined, isPublic: boolean | undefined, optionName: string, ): "pkcs1" | "spki" | "pkcs8" | "sec1" | undefined { if (typeStr === undefined && !required) { return undefined; } else if (typeStr === "pkcs1") { if (keyType !== undefined && keyType !== "rsa") { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( typeStr, "can only be used for RSA keys", ); } return "pkcs1"; } else if (typeStr === "spki" && isPublic !== false) { return "spki"; } else if (typeStr === "pkcs8" && isPublic !== true) { return "pkcs8"; } else if (typeStr === "sec1" && isPublic !== true) { if (keyType !== undefined && keyType !== "ec") { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( typeStr, "can only be used for EC keys", ); } return "sec1"; } throw new ERR_INVALID_ARG_VALUE(optionName, typeStr); } // Parses the public key encoding based on an object. keyType must be undefined // when this is used to parse an input encoding and must be a valid key type if // used to parse an output encoding. function parsePublicKeyEncoding( enc: { cipher?: string; passphrase?: string | Buffer | ArrayBuffer | ArrayBufferView; encoding?: BufferEncoding | "buffer"; format?: string; type?: string; }, keyType: string | undefined, objName?: string, ) { return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName); } // Parses the private key encoding based on an object. keyType must be undefined // when this is used to parse an input encoding and must be a valid key type if // used to parse an output encoding. function parsePrivateKeyEncoding( enc: { cipher?: string; passphrase?: string | Buffer | ArrayBuffer | ArrayBufferView; encoding?: BufferEncoding | "buffer"; format?: string; type?: string; }, keyType: string | undefined, objName?: string, ) { return parseKeyEncoding(enc, keyType, false, objName); } export function createPrivateKey( key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, ): PrivateKeyObject { const res = prepareAsymmetricKey(key, kCreatePrivate); if ("handle" in res) { const type = op_node_key_type(res.handle); if (type === "private") { return new PrivateKeyObject(res.handle); } else { throw new TypeError(`Can not create private key from ${type} key`); } } else { const handle = op_node_create_private_key( res.data, res.format, res.type ?? "", res.passphrase, ); return new PrivateKeyObject(handle); } } export function createPublicKey( key: PublicKeyInput | string | Buffer | JsonWebKeyInput, ): PublicKeyObject { const res = prepareAsymmetricKey( key, kCreatePublic, ); if ("handle" in res) { const type = op_node_key_type(res.handle); if (type === "private") { const handle = op_node_derive_public_key_from_private_key(res.handle); return new PublicKeyObject(handle); } else if (type === "public") { return new PublicKeyObject(res.handle); } else { throw new TypeError(`Can not create private key from ${type} key`); } } else { const handle = op_node_create_public_key( res.data, res.format, res.type ?? "", ); return new PublicKeyObject(handle); } } function getKeyTypes(allowKeyObject: boolean, bufferOnly = false) { const types = [ "ArrayBuffer", "Buffer", "TypedArray", "DataView", "string", // Only if bufferOnly == false "KeyObject", // Only if allowKeyObject == true && bufferOnly == false "CryptoKey", // Only if allowKeyObject == true && bufferOnly == false ]; if (bufferOnly) { return types.slice(0, 4); } else if (!allowKeyObject) { return types.slice(0, 5); } return types; } export function prepareSecretKey( key: string | ArrayBufferView | ArrayBuffer | KeyObject | CryptoKey, encoding: string | undefined, bufferOnly = false, ): Buffer | ArrayBuffer | ArrayBufferView | KeyObjectHandle { if (!bufferOnly) { if (isKeyObject(key)) { if (key.type !== "secret") { throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "secret"); } return key[kHandle]; } else if (isCryptoKey(key)) { notImplemented("using CryptoKey as input"); } } if ( typeof key !== "string" && !isArrayBufferView(key) && !isAnyArrayBuffer(key) ) { throw new ERR_INVALID_ARG_TYPE( "key", getKeyTypes(!bufferOnly, bufferOnly), key, ); } return getArrayBufferOrView(key, "key", encoding); } export class SecretKeyObject extends KeyObject { constructor(handle: KeyObjectHandle) { super("secret", handle); } get symmetricKeySize() { return op_node_get_symmetric_key_size(this[kHandle]); } get asymmetricKeyType() { return undefined; } export(options?: { format?: "buffer" | "jwk" }): Buffer | JsonWebKey { let format: "buffer" | "jwk" = "buffer"; if (options !== undefined) { validateObject(options, "options"); validateOneOf( options.format, "options.format", [undefined, "buffer", "jwk"], ); format = options.format ?? "buffer"; } switch (format) { case "buffer": return Buffer.from(op_node_export_secret_key(this[kHandle])); case "jwk": return { kty: "oct", k: op_node_export_secret_key_b64url(this[kHandle]), }; } } } class AsymmetricKeyObject extends KeyObject { constructor(type: KeyObjectType, handle: KeyObjectHandle) { super(type, handle); } get asymmetricKeyType() { return op_node_get_asymmetric_key_type(this[kHandle]); } get asymmetricKeyDetails() { return op_node_get_asymmetric_key_details(this[kHandle]); } } export class PrivateKeyObject extends AsymmetricKeyObject { constructor(handle: KeyObjectHandle) { super("private", handle); } export(options: JwkKeyExportOptions | KeyExportOptions<KeyFormat>) { if (options && options.format === "jwk") { notImplemented("jwk private key export not implemented"); } const { format, type, } = parsePrivateKeyEncoding(options, this.asymmetricKeyType); if (format === "pem") { return op_node_export_private_key_pem(this[kHandle], type); } else { return Buffer.from(op_node_export_private_key_der(this[kHandle], type)); } } } export class PublicKeyObject extends AsymmetricKeyObject { constructor(handle: KeyObjectHandle) { super("public", handle); } export(options: JwkKeyExportOptions | KeyExportOptions<KeyFormat>) { if (options && options.format === "jwk") { return op_node_export_public_key_jwk(this[kHandle]); } const { format, type, } = parsePublicKeyEncoding(options, this.asymmetricKeyType); if (format === "pem") { return op_node_export_public_key_pem(this[kHandle], type); } else { return Buffer.from(op_node_export_public_key_der(this[kHandle], type)); } } } export function createSecretKey( key: string | ArrayBufferView | ArrayBuffer | KeyObject | CryptoKey, encoding?: string, ): KeyObject { const preparedKey = prepareSecretKey(key, encoding, true); if (isArrayBufferView(preparedKey) || isAnyArrayBuffer(preparedKey)) { const handle = op_node_create_secret_key(preparedKey); return new SecretKeyObject(handle); } else { const type = op_node_key_type(preparedKey); if (type === "secret") { return new SecretKeyObject(preparedKey); } else { throw new TypeError(`can not create secret key from ${type} key`); } } } export default { createPrivateKey, createPublicKey, createSecretKey, KeyObject, prepareSecretKey, SecretKeyObject, PrivateKeyObject, PublicKeyObject, };