/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
lib/internal/crypto/keys.js
1 378 строк
40 KB
Filip Skokan
crypto: read WebCrypto inputs through primordials
09 авг 2026, 22:10
09 авг 2026, 22:10
e04663a
Код
Авторство
О чём код?
'use strict'; const { ArrayPrototypeSlice, ObjectDefineProperties, ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, StringPrototypeIncludes, StringPrototypeStartsWith, SymbolToStringTag, Uint8Array, } = primordials; const { KeyObjectHandle, createNativeKeyObjectClass, // eslint-disable-next-line no-restricted-syntax -- intended here getKeyObjectSlots: nativeGetKeyObjectSlots, createCryptoKeyClass, // eslint-disable-next-line no-restricted-syntax -- intended here getCryptoKeySlots: nativeGetCryptoKeySlots, kKeyTypeSecret, kKeyTypePublic, kKeyTypePrivate, kKeyFormatPEM, kKeyFormatDER, kKeyFormatJWK, kKeyFormatRawPublic, kKeyFormatRawPrivate, kKeyFormatRawSeed, kKeyFormatStore, kKeyEncodingPKCS1, kKeyEncodingPKCS8, kKeyEncodingSPKI, kKeyEncodingSEC1, } = internalBinding('crypto'); const { crypto: { POINT_CONVERSION_COMPRESSED, POINT_CONVERSION_UNCOMPRESSED, }, } = internalBinding('constants'); const { validateObject, validateOneOf, validateString, } = require('internal/validators'); const { codes: { ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS, ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_THIS, }, } = require('internal/errors'); const { getArrayBufferOrView, bigIntArrayToUnsignedBigInt, normalizeAlgorithm, hasAnyNotIn, getUsagesMask, getUsagesFromMask, hasUsage, toUsagesSet, } = require('internal/crypto/util'); const { isAnyArrayBuffer, isArrayBufferView, } = require('internal/util/types'); const { fileURLToPath, getURLHref, isURLInstance, } = require('internal/url'); const { customInspectSymbol: kInspect, kEnumerableProperty, kEmptyObject, lazyDOMException, } = require('internal/util'); const { inspect } = require('internal/util/inspect'); // Key input contexts. const kConsumePublic = 0; const kConsumePrivate = 1; const kCreatePublic = 2; const kCreatePrivate = 3; const encodingNames = []; for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) encodingNames[m[0]] = m[1]; // KeyObject state lives on the native NativeKeyObject base class. JS reads // the native type enum and a KeyObjectHandle in one call and caches that // slot tuple in a private field so no forgeable own Symbols are exposed on // public KeyObject instances. let getKeyObjectSlots; // Populated by the createNativeKeyObjectClass callback. const kKeyObjectSlotType = 0; const kKeyObjectSlotHandle = 1; // The native slot tuple stops at kKeyObjectSlotHandle. The remaining entries // are JS-side lazy cache slots derived from the KeyObjectHandle on first use. const kKeyObjectSlotSymmetricKeySize = 2; const kKeyObjectSlotAsymmetricKeyType = 3; const kKeyObjectSlotAsymmetricKeyDetails = 4; function normalizeKeyDetails(details = kEmptyObject) { if (details.publicExponent !== undefined) { return { __proto__: null, ...details, publicExponent: bigIntArrayToUnsignedBigInt(new Uint8Array(details.publicExponent)), }; } return details; } // Creating the KeyObject class is a little complicated due to inheritance // and the fact that KeyObjects should be transferable between threads, // which requires the KeyObject base class to be implemented in C++. // The creation requires a callback to make sure that the NativeKeyObject // base class cannot exist without the other KeyObject implementations. const { 0: KeyObject, 1: SecretKeyObject, 2: PublicKeyObject, 3: PrivateKeyObject, } = createNativeKeyObjectClass((NativeKeyObject) => { let webidl; // Publicly visible KeyObject class. class KeyObject extends NativeKeyObject { #slots; constructor(type, handle) { if (type !== 'secret' && type !== 'public' && type !== 'private') throw new ERR_INVALID_ARG_VALUE('type', type); if (typeof handle !== 'object' || !(handle instanceof KeyObjectHandle)) throw new ERR_INVALID_ARG_TYPE('handle', 'object', handle); super(handle); } get type() { return getKeyObjectType(this); } toCryptoKey(algorithm, extractable, keyUsages) { webidl ??= require('internal/crypto/webidl'); algorithm = normalizeAlgorithm(webidl.converters.AlgorithmIdentifier(algorithm), 'importKey'); extractable = webidl.converters.boolean(extractable); keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages); const type = getKeyObjectType(this); const handle = getKeyObjectHandle(this); switch (type) { case 'secret': return toCryptoKeySecret( handle, algorithm, extractable, keyUsages); case 'public': // Fall through case 'private': return toCryptoKey( handle, algorithm, extractable, keyUsages); } } static from(key) { if (!isCryptoKey(key)) throw new ERR_INVALID_ARG_TYPE('key', 'CryptoKey', key); if (!getCryptoKeyExtractable(key)) { throw new ERR_INVALID_ARG_VALUE( 'key', key, 'must be an extractable CryptoKey'); } const handle = getCryptoKeyHandle(key); switch (getCryptoKeyType(key)) { /* eslint-disable no-use-before-define */ case 'secret': return new SecretKeyObject(handle); case 'public': return new PublicKeyObject(handle); case 'private': return new PrivateKeyObject(handle); /* eslint-enable no-use-before-define */ } } equals(otherKeyObject) { if (!isKeyObject(otherKeyObject)) { throw new ERR_INVALID_ARG_TYPE( 'otherKeyObject', 'KeyObject', otherKeyObject); } const slots = getKeyObjectSlots(this); const otherSlots = getKeyObjectSlots(otherKeyObject); return slots[kKeyObjectSlotType] === otherSlots[kKeyObjectSlotType] && slots[kKeyObjectSlotHandle].equals( otherSlots[kKeyObjectSlotHandle]); } static { getKeyObjectSlots = (key) => { if (!key || typeof key !== 'object') throw new ERR_INVALID_THIS('KeyObject'); if (#slots in key) { const cached = key.#slots; if (cached !== undefined) return cached; } const slots = nativeGetKeyObjectSlots(key); key.#slots = slots; return slots; }; } } ObjectDefineProperties(KeyObject.prototype, { [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'KeyObject', }, }); class SecretKeyObject extends KeyObject { constructor(handle) { super('secret', handle); } get symmetricKeySize() { return getKeyObjectSymmetricKeySize(this); } export(options) { const handle = getKeyObjectHandle(this); if (options !== undefined) { validateObject(options, 'options'); validateOneOf( options.format, 'options.format', [undefined, 'buffer', 'jwk']); if (options.format === 'jwk') { return handle.exportJwk({}, false); } } return handle.export(); } } class AsymmetricKeyObject extends KeyObject { get asymmetricKeyType() { return getKeyObjectAsymmetricKeyType(this); } get asymmetricKeyDetails() { return { ...getKeyObjectAsymmetricKeyDetails(this) }; } } class PublicKeyObject extends AsymmetricKeyObject { constructor(handle) { super('public', handle); } export(options) { switch (options?.format) { case 'jwk': return getKeyObjectHandle(this).exportJwk({}, false); case 'raw-public': { const handle = getKeyObjectHandle(this); const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); if (asymmetricKeyType === 'ec') { const { type = 'uncompressed' } = options; validateOneOf(type, 'options.type', ['compressed', 'uncompressed']); const form = type === 'compressed' ? POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED; return handle.exportECPublicRaw(form); } return handle.rawPublicKey(); } default: { const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); const handle = getKeyObjectHandle(this); const { format, type, } = parsePublicKeyEncoding(options, asymmetricKeyType); return handle.export(format, type); } } } } class PrivateKeyObject extends AsymmetricKeyObject { constructor(handle) { super('private', handle); } export(options) { if (options?.passphrase !== undefined && options.format !== 'pem' && options.format !== 'der') { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( options.format, 'does not support encryption'); } switch (options?.format) { case 'jwk': return getKeyObjectHandle(this).exportJwk({}, false); case 'raw-private': { const handle = getKeyObjectHandle(this); const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); if (asymmetricKeyType === 'ec') { return handle.exportECPrivateRaw(); } return handle.rawPrivateKey(); } case 'raw-seed': return getKeyObjectHandle(this).rawSeed(); default: { const asymmetricKeyType = getKeyObjectAsymmetricKeyType(this); const handle = getKeyObjectHandle(this); const { format, type, cipher, passphrase, } = parsePrivateKeyEncoding(options, asymmetricKeyType); return handle.export(format, type, cipher, passphrase); } } } } return [KeyObject, SecretKeyObject, PublicKeyObject, PrivateKeyObject]; }); function parseKeyFormat(formatStr, defaultFormat, optionName) { if (formatStr === undefined && defaultFormat !== undefined) return defaultFormat; else if (formatStr === 'pem') return kKeyFormatPEM; else if (formatStr === 'der') return kKeyFormatDER; else if (formatStr === 'jwk') return kKeyFormatJWK; else if (formatStr === 'raw-public') return kKeyFormatRawPublic; else if (formatStr === 'raw-private') return kKeyFormatRawPrivate; else if (formatStr === 'raw-seed') return kKeyFormatRawSeed; throw new ERR_INVALID_ARG_VALUE(optionName, formatStr); } function parseKeyType(typeStr, required, keyType, isPublic, optionName) { 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 kKeyEncodingPKCS1; } else if (typeStr === 'spki' && isPublic !== false) { return kKeyEncodingSPKI; } else if (typeStr === 'pkcs8' && isPublic !== true) { return kKeyEncodingPKCS8; } 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 kKeyEncodingSEC1; } throw new ERR_INVALID_ARG_VALUE(optionName, typeStr); } function option(name, prefix) { return prefix === undefined ? `options.${name}` : `${prefix}.${name}`; } function parseKeyFormatAndType(enc, keyType, isPublic, objName) { const { format: formatStr, type: typeStr } = enc; const isInput = keyType === undefined; const format = parseKeyFormat(formatStr, isInput ? kKeyFormatPEM : undefined, option('format', objName)); if (format === kKeyFormatRawPublic) { if (isPublic === false) { throw new ERR_INVALID_ARG_VALUE(option('format', objName), 'raw-public'); } let type; if (typeStr === undefined || typeStr === 'uncompressed') { type = POINT_CONVERSION_UNCOMPRESSED; } else if (typeStr === 'compressed') { type = POINT_CONVERSION_COMPRESSED; } else { throw new ERR_INVALID_ARG_VALUE(option('type', objName), typeStr); } return { format, type }; } if (format === kKeyFormatRawPrivate || format === kKeyFormatRawSeed) { if (isPublic === true) { throw new ERR_INVALID_ARG_VALUE( option('format', objName), format === kKeyFormatRawPrivate ? 'raw-private' : 'raw-seed'); } if (typeStr !== undefined) { throw new ERR_INVALID_ARG_VALUE(option('type', objName), typeStr); } return { format }; } const isRequired = (!isInput || format === kKeyFormatDER) && format !== kKeyFormatJWK; const type = parseKeyType(typeStr, isRequired, keyType, isPublic, option('type', objName)); return { format, type }; } function isStringOrBuffer(val) { return typeof val === 'string' || isArrayBufferView(val) || isAnyArrayBuffer(val); } function parseKeyEncoding(enc, keyType, isPublic, objName) { validateObject(enc, 'options'); const isInput = keyType === undefined; const { format, type, } = parseKeyFormatAndType(enc, keyType, isPublic, objName); let cipher, passphrase, encoding; if (isPublic !== true) { ({ cipher, passphrase, encoding } = enc); if (format === kKeyFormatRawPrivate || format === kKeyFormatRawSeed) { if (cipher != null || passphrase !== undefined) { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( 'raw format', 'does not support encryption'); } return { format, type }; } if (!isInput) { if (cipher != null) { if (typeof cipher !== 'string') throw new ERR_INVALID_ARG_VALUE(option('cipher', objName), cipher); if (format === kKeyFormatDER && (type === kKeyEncodingPKCS1 || type === kKeyEncodingSEC1)) { throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS( encodingNames[type], 'does not support encryption'); } } else if (passphrase !== undefined) { throw new ERR_INVALID_ARG_VALUE( option('cipher', objName), cipher, 'is required when a passphrase is specified'); } } 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 { format, type, cipher, passphrase }; } // 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, keyType, objName) { 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, keyType, objName) { return parseKeyEncoding(enc, keyType, false, objName); } function validateAsymmetricKeyType(type, ctx, key) { if (ctx === kCreatePrivate) { throw new ERR_INVALID_ARG_TYPE( 'key', getKeyTypes({ allowKeyObject: false, allowURL: true }), key, ); } if (type !== 'private') { if (ctx === kConsumePrivate || ctx === kCreatePublic) throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private'); if (type !== 'public') { throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'private or public'); } } } const kKeyTypesBufferOnly = [ 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView', ]; function getKeyTypes({ allowKeyObject, allowURL = false }) { return [ ...kKeyTypesBufferOnly, 'string', ...(allowKeyObject ? ['KeyObject'] : []), ...(allowURL ? ['URL'] : []), ]; } function prepareStorePrivateKey(url, name, passphrase, encoding, properties) { // Read the canonical serialization from private URL state. Public accessors // can be overridden by subclasses or through prototype mutation. let uri = getURLHref(url); if (StringPrototypeStartsWith(uri, 'file:')) uri = fileURLToPath(uri); validateString(uri, name); // The URI is handed to OpenSSL as a NUL-terminated C string. Reject embedded // NUL bytes, which `fileURLToPath()` happily decodes from `%00`, rather than // letting the URI be silently truncated. if (StringPrototypeIncludes(uri, '\u0000')) { throw new ERR_INVALID_ARG_VALUE( name, url, 'must be a URL without null bytes'); } if (passphrase !== undefined) { if (!isStringOrBuffer(passphrase)) { throw new ERR_INVALID_ARG_VALUE(option('passphrase', name), passphrase); } passphrase = getArrayBufferOrView( passphrase, option('passphrase', name), encoding); } if (properties !== undefined) { validateString(properties, option('properties', name)); // Reaches OpenSSL as a NUL-terminated C string, same as the URI above. if (StringPrototypeIncludes(properties, '\u0000')) { throw new ERR_INVALID_ARG_VALUE( option('properties', name), properties, 'must be a string without null bytes'); } } return { data: { uri, properties: properties ?? null, }, format: kKeyFormatStore, passphrase: passphrase ?? null, }; } function prepareAsymmetricKey(key, ctx, name = 'key') { const isPrivateKeyInput = ctx === kConsumePrivate || ctx === kCreatePrivate; if (isKeyObject(key)) { // Best case: A key object, as simple as that. const type = getKeyObjectType(key); validateAsymmetricKeyType(type, ctx, key); return { data: getKeyObjectHandle(key) }; } if (isPrivateKeyInput && isURLInstance(key)) { // A private key referenced by a URI for an OpenSSL STORE loader. return prepareStorePrivateKey(key, name); } if (isStringOrBuffer(key)) { // Expect PEM by default, mostly for backward compatibility. return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, name) }; } if (typeof key === 'object') { const { key: data, encoding, format, properties } = 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)) { const type = getKeyObjectType(data); validateAsymmetricKeyType(type, ctx, data); return { data: getKeyObjectHandle(data) }; } if (isPrivateKeyInput && isURLInstance(data)) { // A private key referenced by a URI for an OpenSSL STORE loader, with // an optional passphrase/PIN. return prepareStorePrivateKey( data, name, key.passphrase, encoding, properties); } if (format === 'jwk') { validateObject(data, `${name}.key`); return { data, format: kKeyFormatJWK }; } else if (format === 'raw-public' || format === 'raw-private' || format === 'raw-seed') { if ((ctx === kConsumePrivate || ctx === kCreatePrivate) && format === 'raw-public') { throw new ERR_INVALID_ARG_VALUE(`${name}.format`, format); } if (!isArrayBufferView(data) && !isAnyArrayBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( `${name}.key`, ['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'], data); } validateString(key.asymmetricKeyType, `${name}.asymmetricKeyType`); if (key.asymmetricKeyType === 'ec') { validateString(key.namedCurve, `${name}.namedCurve`); } const rawFormat = parseKeyFormat(format, undefined, `${name}.format`); return { data: getArrayBufferOrView(data, `${name}.key`), format: rawFormat, type: key.asymmetricKeyType, namedCurve: key.namedCurve ?? null, }; } // Either PEM or DER using PKCS#1 or SPKI. if (!isStringOrBuffer(data)) { throw new ERR_INVALID_ARG_TYPE( `${name}.key`, getKeyTypes({ allowKeyObject: ctx !== kCreatePrivate, allowURL: isPrivateKeyInput, }), data); } const isPublic = (ctx === kConsumePrivate || ctx === kCreatePrivate) ? false : undefined; return { data: getArrayBufferOrView(data, `${name}.key`, encoding), ...parseKeyEncoding(key, undefined, isPublic, name), }; } throw new ERR_INVALID_ARG_TYPE( name, getKeyTypes({ allowKeyObject: ctx !== kCreatePrivate, allowURL: isPrivateKeyInput, }), key); } function preparePrivateKey(key, name) { return prepareAsymmetricKey(key, kConsumePrivate, name); } function preparePublicOrPrivateKey(key, name) { return prepareAsymmetricKey(key, kConsumePublic, name); } function prepareSecretKey(key, encoding, bufferOnly = false) { if (!bufferOnly) { if (isKeyObject(key)) { const type = getKeyObjectType(key); if (type !== 'secret') throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(type, 'secret'); return getKeyObjectHandle(key); } } if (typeof key !== 'string' && !isArrayBufferView(key) && !isAnyArrayBuffer(key)) { throw new ERR_INVALID_ARG_TYPE( 'key', bufferOnly ? kKeyTypesBufferOnly : getKeyTypes({ allowKeyObject: true }), key); } return getArrayBufferOrView(key, 'key', encoding); } function createSecretKey(key, encoding) { key = prepareSecretKey(key, encoding, true); const handle = new KeyObjectHandle(); handle.init(kKeyTypeSecret, key); return new SecretKeyObject(handle); } function createPublicKey(key) { const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePublic); const handle = new KeyObjectHandle(); handle.init(kKeyTypePublic, data, format ?? null, type ?? null, passphrase ?? null, namedCurve ?? null); return new PublicKeyObject(handle); } /** * Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the * algorithm-specific Web Crypto import path. * @param {KeyObjectHandle} keyData * @param {object} algorithm * @param {boolean} extractable * @param {string[]} keyUsages * @returns {CryptoKey} */ function toCryptoKeySecret( keyData, algorithm, extractable, keyUsages, ) { let result; switch (algorithm.name) { case 'HMAC': // Fall through case 'KMAC128': // Fall through case 'KMAC256': result = require('internal/crypto/mac') .macImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; case 'AES-CTR': // Fall through case 'AES-CBC': // Fall through case 'AES-GCM': // Fall through case 'AES-KW': // Fall through case 'AES-OCB': result = require('internal/crypto/aes') .aesImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); break; case 'ChaCha20-Poly1305': result = require('internal/crypto/chacha20_poly1305') .c20pImportKey(algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); break; case 'HKDF': // Fall through case 'PBKDF2': // Fall through case 'Argon2d': // Fall through case 'Argon2i': // Fall through case 'Argon2id': result = importGenericSecretKey( algorithm, 'KeyObjectHandle', keyData, extractable, keyUsages); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } if (getCryptoKeyUsagesMask(result) === 0) { throw lazyDOMException( `Usages cannot be empty when importing a ${getCryptoKeyType(result)} key.`, 'SyntaxError'); } return result; } /** * Converts an asymmetric KeyObjectHandle to a CryptoKey by dispatching to * the algorithm-specific Web Crypto import path. This preserves the same * algorithm and usage validation used by SubtleCrypto.importKey(). * @param {KeyObjectHandle} keyData * @param {object} algorithm * @param {boolean} extractable * @param {string[]} keyUsages * @returns {CryptoKey} */ function toCryptoKey( keyData, algorithm, extractable, keyUsages, ) { let result; switch (algorithm.name) { case 'RSASSA-PKCS1-v1_5': // Fall through case 'RSA-PSS': // Fall through case 'RSA-OAEP': result = require('internal/crypto/rsa') .rsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; case 'ECDSA': // Fall through case 'ECDH': result = require('internal/crypto/ec') .ecImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; case 'Ed25519': // Fall through case 'Ed448': // Fall through case 'X25519': // Fall through case 'X448': result = require('internal/crypto/cfrg') .cfrgImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; case 'ML-DSA-44': // Fall through case 'ML-DSA-65': // Fall through case 'ML-DSA-87': result = require('internal/crypto/ml_dsa') .mlDsaImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; case 'ML-KEM-512': // Fall through case 'ML-KEM-768': // Fall through case 'ML-KEM-1024': result = require('internal/crypto/ml_kem') .mlKemImportKey('KeyObjectHandle', keyData, algorithm, extractable, keyUsages); break; default: throw lazyDOMException('Unrecognized algorithm name', 'NotSupportedError'); } const resultType = getCryptoKeyType(result); if (resultType === 'private' && getCryptoKeyUsagesMask(result) === 0) { throw lazyDOMException( `Usages cannot be empty when importing a ${resultType} key.`, 'SyntaxError'); } return result; } /** * Derives a public CryptoKey from a private CryptoKey. The resulting key uses * the private key's algorithm, is extractable, and validates the requested * public usages through the shared asymmetric import path. * @param {CryptoKey} key * @param {string[]} keyUsages * @returns {CryptoKey} */ function toPublicCryptoKey(key, keyUsages) { const handle = new KeyObjectHandle(); handle.init(kKeyTypePublic, getCryptoKeyHandle(key), null, null, null, null); return toCryptoKey( handle, getCryptoKeyAlgorithm(key), true, keyUsages); } function createPrivateKey(key) { const { format, type, data, passphrase, namedCurve } = prepareAsymmetricKey(key, kCreatePrivate); const handle = new KeyObjectHandle(); handle.init(kKeyTypePrivate, data, format ?? null, type ?? null, passphrase ?? null, namedCurve ?? null); return new PrivateKeyObject(handle); } function keyObjectTypeToString(type) { switch (type) { case kKeyTypeSecret: return 'secret'; case kKeyTypePublic: return 'public'; case kKeyTypePrivate: return 'private'; default: { const assert = require('internal/assert'); assert.fail('Unreachable code'); } } } // The helpers below return a KeyObject's native-backed slot values, // populating the per-instance cache on first access via a single native // call. The public getters delegate to these helpers, and internal // consumers use them directly to avoid user-replaceable public accessors. // Derived metadata such as key size and asymmetric key details is expanded // lazily from the cached KeyObjectHandle. The public asymmetric key details // getter returns a clone so the cached details object stays internal. /** * Returns the KeyObject's native type slot as a string. * @param {KeyObject} key * @returns {'secret' | 'public' | 'private'} */ function getKeyObjectType(key) { return keyObjectTypeToString(getKeyObjectSlots(key)[kKeyObjectSlotType]); } /** * Returns the KeyObjectHandle wrapping the KeyObject's underlying key * material. * @param {KeyObject} key * @returns {KeyObjectHandle} */ function getKeyObjectHandle(key) { return getKeyObjectSlots(key)[kKeyObjectSlotHandle]; } /** * Returns the KeyObject's symmetric key size, bypassing the public * `symmetricKeySize` getter. The value is derived lazily from the cached * KeyObjectHandle. * @param {SecretKeyObject} key * @returns {number} */ function getKeyObjectSymmetricKeySize(key) { const slots = getKeyObjectSlots(key); if (slots[kKeyObjectSlotType] !== kKeyTypeSecret) throw new ERR_INVALID_THIS('SecretKeyObject'); let cached = slots[kKeyObjectSlotSymmetricKeySize]; if (cached === undefined) { cached = slots[kKeyObjectSlotHandle].getSymmetricKeySize(); slots[kKeyObjectSlotSymmetricKeySize] = cached; } return cached; } /** * Returns the KeyObject's asymmetric key type, bypassing the public * `asymmetricKeyType` getter. The value is derived lazily from the cached * KeyObjectHandle. * @param {PublicKeyObject|PrivateKeyObject} key * @returns {string} */ function getKeyObjectAsymmetricKeyType(key) { const slots = getKeyObjectSlots(key); if (slots[kKeyObjectSlotType] === kKeyTypeSecret) throw new ERR_INVALID_THIS('AsymmetricKeyObject'); let cached = slots[kKeyObjectSlotAsymmetricKeyType]; if (cached === undefined) { cached = slots[kKeyObjectSlotHandle].getAsymmetricKeyType(); slots[kKeyObjectSlotAsymmetricKeyType] = cached; } return cached; } /** * Returns the KeyObject's cached asymmetric key details, bypassing the * public `asymmetricKeyDetails` getter (which returns a cloned copy). * The value is derived lazily from the cached KeyObjectHandle. * @param {PublicKeyObject|PrivateKeyObject} key * @returns {object} */ function getKeyObjectAsymmetricKeyDetails(key) { const slots = getKeyObjectSlots(key); if (slots[kKeyObjectSlotType] === kKeyTypeSecret) throw new ERR_INVALID_THIS('AsymmetricKeyObject'); let cached = slots[kKeyObjectSlotAsymmetricKeyDetails]; if (cached === undefined) { let asymmetricKeyType = slots[kKeyObjectSlotAsymmetricKeyType]; if (asymmetricKeyType === undefined) { asymmetricKeyType = slots[kKeyObjectSlotHandle].getAsymmetricKeyType(); slots[kKeyObjectSlotAsymmetricKeyType] = asymmetricKeyType; } switch (asymmetricKeyType) { case 'rsa': case 'rsa-pss': case 'dsa': case 'ec': cached = normalizeKeyDetails( slots[kKeyObjectSlotHandle].keyDetail({ __proto__: null }), ); break; default: cached = kEmptyObject; break; } slots[kKeyObjectSlotAsymmetricKeyDetails] = cached; } return cached; } function isKeyObject(obj) { if (obj == null || typeof obj !== 'object') return false; try { getKeyObjectSlots(obj); return true; } catch { return false; } } // CryptoKey is a plain JS class whose prototype's [[Prototype]] is // Object.prototype, as Web Crypto requires. Instance storage (type enum, // extractable, algorithm, usages mask, and the KeyObject handle) lives // on a C++ class, NativeCryptoKey, created by createCryptoKeyClass. // InternalCryptoKey is the only constructor we expose to internal // code; it extends NativeCryptoKey to get that storage and then has // its prototype spliced so the chain visible to user code is: // instance -> InternalCryptoKey.prototype // -> CryptoKey.prototype // -> Object.prototype // // All five internal slots are read from C++ in a single call via // `getCryptoKeySlots`. The resulting array is cached in a private // class field on `InternalCryptoKey` so that it is invisible to // reflection (`Object.getOwnPropertySymbols` etc.) and leaves each // CryptoKey's hidden class pristine. The `getCryptoKey{Type, // Extractable,Algorithm,Usages,Handle}` helpers index into that // array and convert native enums/masks back to Web Crypto strings. // The internal algorithm object is stored as a null-prototype clone // so it cannot observe polluted Object.prototype properties. // The public `algorithm` getter caches a cloned dictionary and the // public `usages` getter caches a synthesized array (as Web Crypto // requires repeat reads to return the same object so a consumer's // mutation is visible next time). let getSlots; // Populated by the createCryptoKeyClass callback below. const kSlotType = 0; const kSlotExtractable = 1; const kSlotAlgorithm = 2; const kSlotUsagesMask = 3; const kSlotHandle = 4; const kSlotClonedAlgorithm = 5; const kSlotClonedUsages = 6; const kSlotUsages = 7; function cloneAlgorithm(raw) { const cloned = { ...raw }; if (ObjectPrototypeHasOwnProperty(cloned, 'hash') && cloned.hash !== undefined) { cloned.hash = { ...cloned.hash }; } if (ObjectPrototypeHasOwnProperty(cloned, 'publicExponent') && cloned.publicExponent !== undefined) { cloned.publicExponent = new Uint8Array(cloned.publicExponent); } return cloned; } function cloneInternalAlgorithm(raw) { const cloned = { __proto__: null, ...raw }; if (ObjectPrototypeHasOwnProperty(cloned, 'hash') && cloned.hash !== undefined) { cloned.hash = { __proto__: null, ...cloned.hash }; } if (ObjectPrototypeHasOwnProperty(cloned, 'publicExponent') && cloned.publicExponent !== undefined) { cloned.publicExponent = new Uint8Array(cloned.publicExponent); } return cloned; } const { 0: CryptoKey, 1: InternalCryptoKey, } = createCryptoKeyClass((NativeCryptoKey) => { class CryptoKey { constructor() { throw new ERR_ILLEGAL_CONSTRUCTOR(); } [kInspect](depth, options) { if (depth < 0) return this; const opts = { ...options, depth: options.depth == null ? null : options.depth - 1, }; return `CryptoKey ${inspect({ type: getCryptoKeyType(this), extractable: getCryptoKeyExtractable(this), algorithm: cloneAlgorithm(getCryptoKeyAlgorithm(this)), usages: ArrayPrototypeSlice(getCryptoKeyUsages(this), 0), }, opts)}`; } get type() { return getCryptoKeyType(this); } get extractable() { return getCryptoKeyExtractable(this); } get algorithm() { const slots = getSlots(this); let cached = slots[kSlotClonedAlgorithm]; if (cached === undefined) { cached = cloneAlgorithm(slots[kSlotAlgorithm]); slots[kSlotClonedAlgorithm] = cached; } return cached; } get usages() { const slots = getSlots(this); let cached = slots[kSlotClonedUsages]; if (cached === undefined) { cached = ArrayPrototypeSlice(getCryptoKeyUsagesFromSlots(slots), 0); slots[kSlotClonedUsages] = cached; } return cached; } } class InternalCryptoKey extends NativeCryptoKey { #slots; constructor(handle, algorithm, usagesMask, extractable) { if (algorithm !== undefined) algorithm = cloneInternalAlgorithm(algorithm); super(handle, algorithm, usagesMask, extractable); } static { getSlots = (key) => { if (!key || typeof key !== 'object') throw new ERR_INVALID_THIS('CryptoKey'); if (#slots in key) { const cached = key.#slots; if (cached !== undefined) return cached; } const slots = nativeGetCryptoKeySlots(key); slots[kSlotAlgorithm] = cloneInternalAlgorithm(slots[kSlotAlgorithm]); key.#slots = slots; return slots; }; } } // Hide NativeCryptoKey from user code. InternalCryptoKey.prototype.constructor = CryptoKey; ObjectSetPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype); ObjectDefineProperties(CryptoKey.prototype, { type: kEnumerableProperty, extractable: kEnumerableProperty, algorithm: kEnumerableProperty, usages: kEnumerableProperty, [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'CryptoKey', }, }); return [CryptoKey, InternalCryptoKey]; }); // The helpers below return a CryptoKey's internal slot value, // populating the per-instance cache on first access via a single // native call. The public `type` getter converts the native enum to // the Web Crypto string. The `usages` helper converts the native usage // mask to Web Crypto strings. The public `algorithm` / `usages` getters // on `CryptoKey.prototype` cache their returned objects. /** * Returns the value of a CryptoKey's `[[type]]` internal slot. * @param {CryptoKey} key * @returns {'secret' | 'public' | 'private'} */ function getCryptoKeyType(key) { switch (getSlots(key)[kSlotType]) { case kKeyTypeSecret: return 'secret'; case kKeyTypePublic: return 'public'; case kKeyTypePrivate: return 'private'; default: { const assert = require('internal/assert'); assert.fail('Unreachable code'); } } } /** * Returns the value of a CryptoKey's `[[extractable]]` internal slot. * @param {CryptoKey} key * @returns {boolean} */ function getCryptoKeyExtractable(key) { return getSlots(key)[kSlotExtractable]; } /** * Returns the CryptoKey's `[[algorithm]]` internal slot, bypassing the * public `algorithm` getter (which returns a cloned copy). * @param {CryptoKey} key * @returns {object} */ function getCryptoKeyAlgorithm(key) { return getSlots(key)[kSlotAlgorithm]; } /** * Returns the CryptoKey's native `[[usages]]` mask. * @param {CryptoKey} key * @returns {number} */ function getCryptoKeyUsagesMask(key) { return getSlots(key)[kSlotUsagesMask]; } /** * Returns whether a CryptoKey's `[[usages]]` contains `usage`. * @param {CryptoKey} key * @param {string} usage * @returns {boolean} */ function hasCryptoKeyUsage(key, usage) { return hasUsage(getCryptoKeyUsagesMask(key), usage); } /** * Returns the CryptoKey's cached canonical usages array for internal * consumers, expanding it from the native usage mask on first access. * @param {Array} slots * @returns {string[]} */ function getCryptoKeyUsagesFromSlots(slots) { let usages = slots[kSlotUsages]; if (usages === undefined) { usages = getUsagesFromMask(slots[kSlotUsagesMask]); slots[kSlotUsages] = usages; } return usages; } /** * Returns the CryptoKey's `[[usages]]` internal slot, bypassing the * public `usages` getter (which returns a cloned array). The internal * array is expanded lazily from the native usage mask. * @param {CryptoKey} key * @returns {string[]} */ function getCryptoKeyUsages(key) { return getCryptoKeyUsagesFromSlots(getSlots(key)); } /** * Returns the KeyObjectHandle wrapping the CryptoKey's underlying * key material. * @param {CryptoKey} key * @returns {KeyObjectHandle} */ function getCryptoKeyHandle(key) { return getSlots(key)[kSlotHandle]; } function isCryptoKey(obj) { if (obj == null || typeof obj !== 'object') return false; try { getSlots(obj); return true; } catch { return false; } } function importGenericSecretKey( algorithm, format, keyData, extractable, keyUsages, ) { const usagesSet = toUsagesSet(keyUsages); const { name } = algorithm; if (extractable) throw lazyDOMException(`${name} keys are not extractable`, 'SyntaxError'); if (hasAnyNotIn(usagesSet, ['deriveKey', 'deriveBits'])) { throw lazyDOMException( `Unsupported key usage for a ${name} key`, 'SyntaxError'); } let handle; switch (format) { case 'KeyObjectHandle': { handle = keyData; break; } case 'raw-secret': case 'raw': { handle = new KeyObjectHandle(); handle.init(kKeyTypeSecret, keyData); break; } default: return undefined; } return new InternalCryptoKey( handle, { name }, getUsagesMask(usagesSet), false); } module.exports = { // Public API. createSecretKey, createPublicKey, createPrivateKey, KeyObject, CryptoKey, InternalCryptoKey, // These are designed for internal use only and should not be exposed. parsePublicKeyEncoding, parsePrivateKeyEncoding, parseKeyEncoding, toPublicCryptoKey, preparePrivateKey, preparePublicOrPrivateKey, prepareSecretKey, SecretKeyObject, PublicKeyObject, PrivateKeyObject, isKeyObject, getKeyObjectType, getKeyObjectHandle, getKeyObjectSymmetricKeySize, getKeyObjectAsymmetricKeyType, getKeyObjectAsymmetricKeyDetails, isCryptoKey, getCryptoKeyType, getCryptoKeyExtractable, getCryptoKeyAlgorithm, getCryptoKeyUsages, getCryptoKeyUsagesMask, hasCryptoKeyUsage, getCryptoKeyHandle, importGenericSecretKey, };