nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
832 lines (737 loc) • 22.3 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/crypto/keys.js
import {
KeyObjectHandle,
createNativeKeyObjectClass,
kKeyTypeSecret,
kKeyTypePublic,
kKeyTypePrivate,
kKeyFormatPEM,
kKeyFormatDER,
kKeyFormatJWK,
kKeyEncodingPKCS1,
kKeyEncodingPKCS8,
kKeyEncodingSPKI,
kKeyEncodingSEC1,
} from "nstdlib/stub/binding/crypto";
import {
validateObject,
validateOneOf,
validateString,
} from "nstdlib/lib/internal/validators";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import {
kHandle,
kKeyObject,
getArrayBufferOrView,
bigIntArrayToUnsignedBigInt,
} from "nstdlib/lib/internal/crypto/util";
import {
isAnyArrayBuffer,
isArrayBufferView,
} from "nstdlib/lib/internal/util/types";
import {
markTransferMode,
kClone,
kDeserialize,
} from "nstdlib/lib/internal/worker/js_transferable";
import {
customInspectSymbol as kInspect,
kEnumerableProperty,
} from "nstdlib/lib/internal/util";
import { inspect } from "nstdlib/lib/internal/util/inspect";
import { Buffer } from "nstdlib/lib/buffer";
const {
ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
ERR_CRYPTO_INVALID_JWK,
ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
ERR_ILLEGAL_CONSTRUCTOR,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_THIS,
} = __codes__;
const kAlgorithm = Symbol("kAlgorithm");
const kExtractable = Symbol("kExtractable");
const kKeyType = Symbol("kKeyType");
const kKeyUsages = Symbol("kKeyUsages");
// 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];
// Creating the KeyObject class is a little complicated due to inheritance
// and the fact that KeyObjects should be transferrable 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) => {
// Publicly visible KeyObject class.
class KeyObject extends NativeKeyObject {
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);
this[kKeyType] = type;
Object.defineProperty(this, kHandle, {
__proto__: null,
value: handle,
enumerable: false,
configurable: false,
writable: false,
});
}
get type() {
return this[kKeyType];
}
static from(key) {
if (!isCryptoKey(key))
throw new ERR_INVALID_ARG_TYPE("key", "CryptoKey", key);
return key[kKeyObject];
}
equals(otherKeyObject) {
if (!isKeyObject(otherKeyObject)) {
throw new ERR_INVALID_ARG_TYPE(
"otherKeyObject",
"KeyObject",
otherKeyObject,
);
}
return (
otherKeyObject.type === this.type &&
this[kHandle].equals(otherKeyObject[kHandle])
);
}
}
Object.defineProperties(KeyObject.prototype, {
[Symbol.toStringTag]: {
__proto__: null,
configurable: true,
value: "KeyObject",
},
});
class SecretKeyObject extends KeyObject {
constructor(handle) {
super("secret", handle);
}
get symmetricKeySize() {
return this[kHandle].getSymmetricKeySize();
}
export(options) {
if (options !== undefined) {
validateObject(options, "options");
validateOneOf(options.format, "options.format", [
undefined,
"buffer",
"jwk",
]);
if (options.format === "jwk") {
return this[kHandle].exportJwk({}, false);
}
}
return this[kHandle].export();
}
}
const kAsymmetricKeyType = Symbol("kAsymmetricKeyType");
const kAsymmetricKeyDetails = Symbol("kAsymmetricKeyDetails");
function normalizeKeyDetails(details = {}) {
if (details.publicExponent !== undefined) {
return {
...details,
publicExponent: bigIntArrayToUnsignedBigInt(
new Uint8Array(details.publicExponent),
),
};
}
return details;
}
class AsymmetricKeyObject extends KeyObject {
// eslint-disable-next-line no-useless-constructor
constructor(type, handle) {
super(type, handle);
}
get asymmetricKeyType() {
return (
this[kAsymmetricKeyType] ||
(this[kAsymmetricKeyType] = this[kHandle].getAsymmetricKeyType())
);
}
get asymmetricKeyDetails() {
switch (this.asymmetricKeyType) {
case "rsa":
case "rsa-pss":
case "dsa":
case "ec":
return (
this[kAsymmetricKeyDetails] ||
(this[kAsymmetricKeyDetails] = normalizeKeyDetails(
this[kHandle].keyDetail({}),
))
);
default:
return {};
}
}
}
class PublicKeyObject extends AsymmetricKeyObject {
constructor(handle) {
super("public", handle);
}
export(options) {
if (options && options.format === "jwk") {
return this[kHandle].exportJwk({}, false);
}
const { format, type } = parsePublicKeyEncoding(
options,
this.asymmetricKeyType,
);
return this[kHandle].export(format, type);
}
}
class PrivateKeyObject extends AsymmetricKeyObject {
constructor(handle) {
super("private", handle);
}
export(options) {
if (options && options.format === "jwk") {
if (options.passphrase !== undefined) {
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
"jwk",
"does not support encryption",
);
}
return this[kHandle].exportJwk({}, false);
}
const { format, type, cipher, passphrase } = parsePrivateKeyEncoding(
options,
this.asymmetricKeyType,
);
return this[kHandle].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;
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, objName) {
return objName === undefined
? `options.${name}`
: `options.${objName}.${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),
);
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 (!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);
}
}
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 getKeyObjectHandle(key, ctx) {
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 getKeyTypes(allowKeyObject, 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 Array.prototype.slice.call(types, 0, 4);
} else if (!allowKeyObject) {
return Array.prototype.slice.call(types, 0, 5);
}
return types;
}
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;
}
const handle = new KeyObjectHandle();
const keyType = isPublic ? kKeyTypePublic : kKeyTypePrivate;
if (!handle.initEDRaw(key.crv, keyData, keyType)) {
throw new ERR_CRYPTO_INVALID_JWK();
}
return handle;
}
if (key.kty === "EC") {
validateString(key.crv, "key.crv");
validateOneOf(key.crv, "key.crv", ["P-256", "secp256k1", "P-384", "P-521"]);
validateString(key.x, "key.x");
validateString(key.y, "key.y");
const jwk = {
kty: key.kty,
crv: key.crv,
x: key.x,
y: key.y,
};
if (!isPublic) {
validateString(key.d, "key.d");
jwk.d = key.d;
}
const handle = new KeyObjectHandle();
const type = handle.initJwk(jwk, jwk.crv);
if (type === undefined) throw new ERR_CRYPTO_INVALID_JWK();
return handle;
}
// 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;
}
const handle = new KeyObjectHandle();
const type = handle.initJwk(jwk);
if (type === undefined) throw new ERR_CRYPTO_INVALID_JWK();
return handle;
}
function prepareAsymmetricKey(key, ctx) {
if (isKeyObject(key)) {
// Best case: A key object, as simple as that.
return { data: getKeyObjectHandle(key, ctx) };
} else if (isCryptoKey(key)) {
return { data: getKeyObjectHandle(key[kKeyObject], ctx) };
} else if (isStringOrBuffer(key)) {
// Expect PEM by default, mostly for backward compatibility.
return { format: kKeyFormatPEM, data: getArrayBufferOrView(key, "key") };
} else if (typeof key === "object") {
const { key: data, encoding, 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 { data: getKeyObjectHandle(data, ctx) };
else if (isCryptoKey(data))
return { data: getKeyObjectHandle(data[kKeyObject], ctx) };
else if (format === "jwk") {
validateObject(data, "key.key");
return { data: getKeyObjectHandleFromJwk(data, ctx), format: "jwk" };
}
// 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", encoding),
...parseKeyEncoding(key, undefined, isPublic),
};
}
throw new ERR_INVALID_ARG_TYPE(
"key",
getKeyTypes(ctx !== kCreatePrivate),
key,
);
}
function preparePrivateKey(key) {
return prepareAsymmetricKey(key, kConsumePrivate);
}
function preparePublicOrPrivateKey(key) {
return prepareAsymmetricKey(key, kConsumePublic);
}
function prepareSecretKey(key, encoding, bufferOnly = false) {
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)) {
if (key.type !== "secret")
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, "secret");
return key[kKeyObject][kHandle];
}
}
if (
typeof key !== "string" &&
!isArrayBufferView(key) &&
!isAnyArrayBuffer(key)
) {
throw new ERR_INVALID_ARG_TYPE(
"key",
getKeyTypes(!bufferOnly, bufferOnly),
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 } = prepareAsymmetricKey(
key,
kCreatePublic,
);
let handle;
if (format === "jwk") {
handle = data;
} else {
handle = new KeyObjectHandle();
handle.init(kKeyTypePublic, data, format, type, passphrase);
}
return new PublicKeyObject(handle);
}
function createPrivateKey(key) {
const { format, type, data, passphrase } = prepareAsymmetricKey(
key,
kCreatePrivate,
);
let handle;
if (format === "jwk") {
handle = data;
} else {
handle = new KeyObjectHandle();
handle.init(kKeyTypePrivate, data, format, type, passphrase);
}
return new PrivateKeyObject(handle);
}
function isKeyObject(obj) {
return obj != null && obj[kKeyType] !== undefined;
}
// Our implementation of CryptoKey is a simple wrapper around a KeyObject
// that adapts it to the standard interface.
// TODO(@jasnell): Embedder environments like electron may have issues
// here similar to other things like URL. A chromium provided CryptoKey
// will not be recognized as a Node.js CryptoKey, and vice versa. It
// would be fantastic if we could find a way of making those interop.
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: this.type,
extractable: this.extractable,
algorithm: this.algorithm,
usages: this.usages,
},
opts,
)}`;
}
get type() {
if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS("CryptoKey");
return this[kKeyObject].type;
}
get extractable() {
if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS("CryptoKey");
return this[kExtractable];
}
get algorithm() {
if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS("CryptoKey");
return this[kAlgorithm];
}
get usages() {
if (!(this instanceof CryptoKey)) throw new ERR_INVALID_THIS("CryptoKey");
return Array.from(this[kKeyUsages]);
}
}
Object.defineProperties(CryptoKey.prototype, {
type: kEnumerableProperty,
extractable: kEnumerableProperty,
algorithm: kEnumerableProperty,
usages: kEnumerableProperty,
[Symbol.toStringTag]: {
__proto__: null,
configurable: true,
value: "CryptoKey",
},
});
/**
* @param {InternalCryptoKey} key
* @param {KeyObject} keyObject
* @param {object} algorithm
* @param {boolean} extractable
* @param {Set<string>} keyUsages
*/
function defineCryptoKeyProperties(
key,
keyObject,
algorithm,
extractable,
keyUsages,
) {
// Using symbol properties here currently instead of private
// properties because (for now) the performance penalty of
// private fields is still too high.
Object.defineProperties(key, {
[kKeyObject]: {
__proto__: null,
value: keyObject,
enumerable: false,
configurable: false,
writable: false,
},
[kAlgorithm]: {
__proto__: null,
value: algorithm,
enumerable: false,
configurable: false,
writable: false,
},
[kExtractable]: {
__proto__: null,
value: extractable,
enumerable: false,
configurable: false,
writable: false,
},
[kKeyUsages]: {
__proto__: null,
value: keyUsages,
enumerable: false,
configurable: false,
writable: false,
},
});
}
// All internal code must use new InternalCryptoKey to create
// CryptoKey instances. The CryptoKey class is exposed to end
// user code but is not permitted to be constructed directly.
// Using markTransferMode also allows the CryptoKey to be
// cloned to Workers.
class InternalCryptoKey {
constructor(keyObject, algorithm, keyUsages, extractable) {
markTransferMode(this, true, false);
// When constructed during transfer the properties get assigned
// in the kDeserialize call.
if (keyObject) {
defineCryptoKeyProperties(
this,
keyObject,
algorithm,
extractable,
keyUsages,
);
}
}
[kClone]() {
const keyObject = this[kKeyObject];
const algorithm = this[kAlgorithm];
const extractable = this[kExtractable];
const usages = this[kKeyUsages];
return {
data: {
keyObject,
algorithm,
usages,
extractable,
},
deserializeInfo: "internal/crypto/keys:InternalCryptoKey",
};
}
[kDeserialize]({ keyObject, algorithm, usages, extractable }) {
defineCryptoKeyProperties(this, keyObject, algorithm, extractable, usages);
}
}
InternalCryptoKey.prototype.constructor = CryptoKey;
Object.setPrototypeOf(InternalCryptoKey.prototype, CryptoKey.prototype);
function isCryptoKey(obj) {
return obj != null && obj[kKeyObject] !== undefined;
}
export { createSecretKey };
export { createPublicKey };
export { createPrivateKey };
export { KeyObject };
export { CryptoKey };
export { InternalCryptoKey };
export { parsePublicKeyEncoding };
export { parsePrivateKeyEncoding };
export { parseKeyEncoding };
export { preparePrivateKey };
export { preparePublicOrPrivateKey };
export { prepareSecretKey };
export { SecretKeyObject };
export { PublicKeyObject };
export { PrivateKeyObject };
export { isKeyObject };
export { isCryptoKey };