1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-30 16:40:57 -05:00
denoland-deno/ext/node/polyfills/internal/crypto/keys.ts
Luca Casonato 4fa8869f24
feat(ext/node): rewrite crypto keys (#24463)
This completely rewrites how we handle key material in ext/node. Changes
in this
PR:

- **Signing**
  - RSA
  - RSA-PSS 🆕
  - DSA 🆕
  - EC
  - ED25519 🆕
- **Verifying**
  - RSA
  - RSA-PSS 🆕
  - DSA 🆕
  - EC 🆕
  - ED25519 🆕
- **Private key import**
  - Passphrase encrypted private keys 🆕
  - RSA
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - RSA-PSS
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - DSA 🆕
  - EC
    - PEM
    - DER (SEC1) 🆕
    - DER (PKCS#8) 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH
- **Public key import**
  - RSA
    - PEM
    - DER (PKCS#1) 🆕
    - DER (PKCS#8) 🆕
  - RSA-PSS 🆕
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Private key export**
  - RSA 🆕
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Public key export**
  - RSA
  - DSA 🆕
  - EC 🆕
  - X25519 🆕
  - ED25519 🆕
  - DH 🆕
- **Key pair generation**
  - Overhauled, but supported APIs unchanged

This PR adds a lot of new individual functionality. But most importantly
because
of the new key material representation, it is now trivial to add new
algorithms
(as shown by this PR).

Now, when adding a new algorithm, it is also widely supported - for
example
previously we supported ED25519 key pair generation, but we could not
import,
export, sign or verify with ED25519. We can now do all of those things.
2024-08-07 08:43:58 +02:00

756 lines
19 KiB
TypeScript

// 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_private_key,
op_node_create_public_key,
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_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_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,
} 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";
}
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];
}
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") {
notImplemented("using JWK as input");
}
// 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") {
notImplemented("jwk public key export not implemented");
}
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,
};