diff --git a/package-lock.json b/package-lock.json index b4d35ecaf..9d36d0c13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@ava/typescript": "^2.0.0", "@microsoft/api-extractor": "^7.19.4", - "@types/node": "^17.0.35", + "@types/node": "^18.7.6", "@types/rimraf": "^3.0.2", "@types/which": "^2.0.1", "@typescript-eslint/eslint-plugin": "^5.9.1", @@ -1738,9 +1738,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", - "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==", + "version": "18.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.6.tgz", + "integrity": "sha512-EdxgKRXgYsNITy5mjjXjVE/CS8YENSdhiagGrLqjG0pvA2owgJ6i4l7wy/PFZGC0B1/H20lWKN7ONVDNYDZm7A==", "dev": true }, "node_modules/@types/node-forge": { @@ -10881,9 +10881,9 @@ "dev": true }, "@types/node": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.35.tgz", - "integrity": "sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg==", + "version": "18.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.6.tgz", + "integrity": "sha512-EdxgKRXgYsNITy5mjjXjVE/CS8YENSdhiagGrLqjG0pvA2owgJ6i4l7wy/PFZGC0B1/H20lWKN7ONVDNYDZm7A==", "dev": true }, "@types/node-forge": { diff --git a/package.json b/package.json index 9daab7da2..7c756906d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "devDependencies": { "@ava/typescript": "^2.0.0", "@microsoft/api-extractor": "^7.19.4", - "@types/node": "^17.0.35", + "@types/node": "^18.7.6", "@types/rimraf": "^3.0.2", "@types/which": "^2.0.1", "@typescript-eslint/eslint-plugin": "^5.9.1", @@ -53,6 +53,6 @@ "node": ">=16.7" }, "volta": { - "node": "18.2.0" + "node": "18.7.0" } } diff --git a/packages/core/src/standards/crypto.ts b/packages/core/src/standards/crypto.ts index 6fdc2a2b4..2ae254178 100644 --- a/packages/core/src/standards/crypto.ts +++ b/packages/core/src/standards/crypto.ts @@ -15,7 +15,7 @@ const supportedDigests = ["sha-1", "sha-256", "sha-384", "sha-512", "md5"]; export class DigestStream extends WritableStream { readonly digest: Promise; - constructor(algorithm: AlgorithmIdentifier) { + constructor(algorithm: webcrypto.AlgorithmIdentifier) { // Check algorithm supported by Cloudflare Workers let name = typeof algorithm === "string" ? algorithm : algorithm?.name; if (!(name && supportedDigests.includes(name.toLowerCase()))) { @@ -47,11 +47,40 @@ export class DigestStream extends WritableStream { } } -// Workers support non-standard MD5 digests -function digest( - algorithm: AlgorithmIdentifier, - data: BufferSource -): Promise { +const usesModernEd25519 = (async () => { + try { + // Modern versions of Node.js expect `Ed25519` instead of `NODE-ED25519`. + // This will throw a `DOMException` if `NODE-ED25519` should be used + // instead. See https://github.com/nodejs/node/pull/42507. + await webcrypto.subtle.generateKey( + { name: "Ed25519", namedCurve: "Ed25519" }, + false, + ["sign", "verify"] + ); + return true; + } catch { + return false; + } +})(); + +async function ensureValidAlgorithm( + algorithm: webcrypto.AlgorithmIdentifier | webcrypto.EcKeyAlgorithm +): Promise { + if ( + typeof algorithm === "object" && + algorithm.name === "NODE-ED25519" && + "namedCurve" in algorithm && + algorithm.namedCurve === "NODE-ED25519" && + (await usesModernEd25519) + ) { + return { name: "Ed25519", namedCurve: "Ed25519" }; + } + return algorithm; +} + +// Workers support non-standard MD5 digests, see +// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms +const digest: typeof webcrypto.subtle.digest = function (algorithm, data) { const name = typeof algorithm === "string" ? algorithm : algorithm?.name; if (name?.toLowerCase() == "md5") { if (data instanceof ArrayBuffer) data = new Uint8Array(data); @@ -61,22 +90,86 @@ function digest( // If the algorithm isn't MD5, defer to the original function return webcrypto.subtle.digest(algorithm, data); -} +}; -export function createCrypto(blockGlobalRandom = false): typeof webcrypto { - const getRandomValues = assertsInRequest( - webcrypto.getRandomValues.bind(webcrypto), - blockGlobalRandom +// Workers support the NODE-ED25519 algorithm, unlike modern Node versions, see +// https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms +const generateKey: typeof webcrypto.subtle.generateKey = async function ( + algorithm, + extractable, + keyUsages +) { + algorithm = await ensureValidAlgorithm(algorithm); + // @ts-expect-error TypeScript cannot infer the correct overload here + return webcrypto.subtle.generateKey(algorithm, extractable, keyUsages); +}; +const importKey: typeof webcrypto.subtle.importKey = async function ( + format, + keyData, + algorithm, + extractable, + keyUsages +) { + // Cloudflare Workers only allow importing *public* raw Ed25519 keys, see + // https://developers.cloudflare.com/workers/runtime-apis/web-crypto/#supported-algorithms + const forcePublic = + format === "raw" && + typeof algorithm === "object" && + algorithm.name === "NODE-ED25519" && + "namedCurve" in algorithm && + algorithm.namedCurve === "NODE-ED25519"; + + algorithm = await ensureValidAlgorithm(algorithm); + + // @ts-expect-error `public` isn't included in the definitions, but required + // for marking `keyData` as public key material + if (forcePublic) algorithm.public = true; + + return webcrypto.subtle.importKey( + // @ts-expect-error TypeScript cannot infer the correct overload here + format, + keyData, + algorithm, + extractable, + keyUsages ); - const generateKey = assertsInRequest( - webcrypto.subtle.generateKey.bind(webcrypto.subtle), +}; +const sign: typeof webcrypto.subtle.sign = async function ( + algorithm, + key, + data +) { + algorithm = await ensureValidAlgorithm(algorithm); + return webcrypto.subtle.sign(algorithm, key, data); +}; +const verify: typeof webcrypto.subtle.verify = async function ( + algorithm, + key, + signature, + data +) { + algorithm = await ensureValidAlgorithm(algorithm); + return webcrypto.subtle.verify(algorithm, key, signature, data); +}; + +export type WorkerCrypto = typeof webcrypto & { + DigestStream: typeof DigestStream; +}; + +export function createCrypto(blockGlobalRandom = false): WorkerCrypto { + const assertingGetRandomValues = assertsInRequest( + webcrypto.getRandomValues.bind(webcrypto), blockGlobalRandom ); + const assertingGenerateKey = assertsInRequest(generateKey, blockGlobalRandom); const subtle = new Proxy(webcrypto.subtle, { get(target, propertyKey, receiver) { if (propertyKey === "digest") return digest; - if (propertyKey === "generateKey") return generateKey; + if (propertyKey === "generateKey") return assertingGenerateKey; + if (propertyKey === "importKey") return importKey; + if (propertyKey === "sign") return sign; + if (propertyKey === "verify") return verify; let result = Reflect.get(target, propertyKey, receiver); if (typeof result === "function") result = result.bind(webcrypto.subtle); @@ -84,9 +177,9 @@ export function createCrypto(blockGlobalRandom = false): typeof webcrypto { }, }); - return new Proxy(webcrypto, { + return new Proxy(webcrypto as WorkerCrypto, { get(target, propertyKey, receiver) { - if (propertyKey === "getRandomValues") return getRandomValues; + if (propertyKey === "getRandomValues") return assertingGetRandomValues; if (propertyKey === "subtle") return subtle; if (propertyKey === "DigestStream") return DigestStream; diff --git a/packages/core/test/standards/crypto.spec.ts b/packages/core/test/standards/crypto.spec.ts index 4bf025f9b..c2a086041 100644 --- a/packages/core/test/standards/crypto.spec.ts +++ b/packages/core/test/standards/crypto.spec.ts @@ -1,3 +1,4 @@ +import { webcrypto } from "crypto"; import { TextEncoder } from "util"; import { DOMException, DigestStream, createCrypto } from "@miniflare/core"; import { utf8Encode } from "@miniflare/shared-test"; @@ -5,7 +6,7 @@ import test, { Macro } from "ava"; const crypto = createCrypto(); -const digestStreamMacro: Macro<[AlgorithmIdentifier]> = async ( +const digestStreamMacro: Macro<[webcrypto.AlgorithmIdentifier]> = async ( t, algorithm ) => { @@ -69,6 +70,7 @@ test("crypto: provides DigestStream", (t) => { t.is(crypto.DigestStream, DigestStream); }); +// Check digest function modified to add MD5 support const md5Macro: Macro<[BufferSource]> = async (t, data) => { const digest = await crypto.subtle.digest("md5", data); t.is(Buffer.from(digest).toString("hex"), "098f6bcd4621d373cade4e832627b4f6"); @@ -90,6 +92,93 @@ test("crypto: computes other digest", async (t) => { ); }); +// Check generateKey, importKey, sing, verify functions modified to add +// NODE-ED25519 support +test("crypto: generateKey/exportKey: supports NODE-ED25519 algorithm", async (t) => { + const keyPair = await crypto.subtle.generateKey( + { name: "NODE-ED25519", namedCurve: "NODE-ED25519" }, + true, + ["sign", "verify"] + ); + const exported = await crypto.subtle.exportKey("raw", keyPair.publicKey); + t.is(exported.byteLength, 32); +}); +test("crypto: generateKey/exportKey: supports other algorithms", async (t) => { + const key = await crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + const exported = await crypto.subtle.exportKey("raw", key); + t.is(exported.byteLength, 32); +}); + +test("crypto: importKey/exportKey: supports NODE-ED25519 public keys", async (t) => { + const keyData = + "953e73cb91a2494a33cd7180f05d5bbe6b5ca43cc66eb93ca38c6fc83cb18f29"; + const publicKey = await crypto.subtle.importKey( + "raw", + Buffer.from(keyData, "hex"), + { name: "NODE-ED25519", namedCurve: "NODE-ED25519" }, + true, + ["verify"] + ); + const exported = await crypto.subtle.exportKey("raw", publicKey); + t.is(Buffer.from(exported).toString("hex"), keyData); +}); +test("crypto: importKey: fails for NODE-ED25519 private keys", async (t) => { + const keyData = + "f0d3c325a99ef50181faa238e07224ec9fee292e7ebf6585560bab64654ec6209c6afa31187898a43f7ab18c3552c2cd349e912c16c803a2a6ccbd546896fe8e"; + await t.throwsAsync( + crypto.subtle.importKey( + "raw", + Buffer.from(keyData, "hex"), + { name: "NODE-ED25519", namedCurve: "NODE-ED25519" }, + false, + ["sign"] + ) + ); +}); +test("crypto: importKey/exportKey: supports other algorithms", async (t) => { + const keyData = + "464d832870721bcf28649192bec41bd1fd5b32702d6168f21b8585fb566a4be7"; + const key = await crypto.subtle.importKey( + "raw", + Buffer.from(keyData, "hex"), + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + const exported = await crypto.subtle.exportKey("raw", key); + t.is(Buffer.from(exported).toString("hex"), keyData); +}); +test("crypto: sign/verify: supports NODE-ED25519 algorithm", async (t) => { + const algorithm: webcrypto.EcKeyAlgorithm = { + name: "NODE-ED25519", + namedCurve: "NODE-ED25519", + }; + const { privateKey, publicKey } = await crypto.subtle.generateKey( + algorithm, + false, + ["sign", "verify"] + ); + const data = utf8Encode("data"); + const signature = await crypto.subtle.sign(algorithm, privateKey, data); + t.is(signature.byteLength, 64); + t.true(await crypto.subtle.verify(algorithm, publicKey, signature, data)); +}); +test("crypto: sign/verify: supports other algorithm", async (t) => { + const key = await crypto.subtle.generateKey( + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"] + ); + const data = utf8Encode("data"); + const signature = await crypto.subtle.sign("HMAC", key, data); + t.is(signature.byteLength, 32); + t.true(await crypto.subtle.verify("HMAC", key, signature, data)); +}); + // Checking other functions aren't broken by proxy... test("crypto: gets random values", (t) => { @@ -102,13 +191,3 @@ test("crypto: gets random values", (t) => { test("crypto: generates random UUID", (t) => { t.is(crypto.randomUUID().length, 36); }); - -test("crypto: generates aes key", async (t) => { - const key = await crypto.subtle.generateKey( - { name: "aes-gcm", length: 256 }, - true, - ["encrypt", "decrypt"] - ); - const exported = await crypto.subtle.exportKey("raw", key); - t.is(exported.byteLength, 32); -}); diff --git a/types/crypto.d.ts b/types/crypto.d.ts deleted file mode 100644 index eabf6e34c..000000000 --- a/types/crypto.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Types adapted from https://github.com/microsoft/TypeScript/blob/main/lib/lib.webworker.d.ts -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); you may not use -// this file except in compliance with the License. You may obtain a copy of the -// License at http://www.apache.org/licenses/LICENSE-2.0 -// -// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED -// WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -// MERCHANTABLITY OR NON-INFRINGEMENT. -// -// See the Apache Version 2.0 License for specific language governing permissions -// and limitations under the License. - -interface Algorithm { - name: string; -} - -type AlgorithmIdentifier = string | Algorithm; - -type BufferSource = ArrayBufferView | ArrayBuffer; - -interface CryptoKey { - readonly algorithm: Algorithm; - readonly extractable: boolean; - readonly type: string; - readonly usages: string[]; -} - -interface SubtleCrypto { - digest( - algorithm: AlgorithmIdentifier, - data: BufferSource - ): Promise; - exportKey(format: string, key: CryptoKey): Promise; - generateKey( - algorithm: Algorithm, - extractable: boolean, - keyUsages: string[] - ): Promise; -} - -declare module "crypto" { - namespace webcrypto { - const subtle: SubtleCrypto; - function getRandomValues(array: T): T; - function randomUUID(): string; - - class DigestStream { - constructor(algorithm: AlgorithmIdentifier); - readonly digest: Promise; - } - } -} diff --git a/types/webassembly.d.ts b/types/webassembly.d.ts index 1146827c9..2e17aa425 100644 --- a/types/webassembly.d.ts +++ b/types/webassembly.d.ts @@ -14,6 +14,8 @@ // See the Apache Version 2.0 License for specific language governing permissions // and limitations under the License. +type BufferSource = ArrayBufferView | ArrayBuffer; + declare namespace WebAssembly { class Global { constructor(descriptor: GlobalDescriptor, v?: any);