From c8a94ea7fa62e39934b6d078cda78148ff906c89 Mon Sep 17 00:00:00 2001 From: Yuval S Date: Sun, 17 Dec 2023 11:36:41 +0200 Subject: [PATCH 1/7] feat: dynamic webcrypto import - works on browser NOTE: not tested on Node<16 (@peculiar/webcrypto fallback) --- lib/main.js | 7 +++++ lib/toolbox.js | 70 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/lib/main.js b/lib/main.js index 44b85fc2..d823cd95 100644 --- a/lib/main.js +++ b/lib/main.js @@ -1,4 +1,5 @@ import * as utils from "./utils.js"; +import { ready } from "./toolbox.js"; import { Fido2AssertionResult, Fido2AttestationResult, Fido2Result } from "./response.js"; @@ -166,6 +167,7 @@ class Fido2Lib { * @see MdsCollection */ static async addMdsCollection(mdsCollection) { + await ready; if (!(mdsCollection instanceof MdsCollection)) { throw new Error( "expected 'mdsCollection' to be instance of MdsCollection, got: " + @@ -386,6 +388,7 @@ class Fido2Lib { * @private */ static async validateAttestation() { + await ready; const fmt = this.authnrData.get("fmt"); // validate input @@ -542,6 +545,7 @@ class Fido2Lib { * @throws {Error} If parsing or validation fails */ async attestationResult(res, expected) { + await ready; expected.flags = factorToFlags(expected.factor, ["AT"]); delete expected.factor; return await Fido2AttestationResult.create(res, expected); @@ -574,6 +578,7 @@ class Fido2Lib { */ // deno-lint-ignore require-await async assertionResult(res, expected) { + await ready; expected.flags = factorToFlags(expected.factor, []); delete expected.factor; return Fido2AssertionResult.create(res, expected); @@ -594,6 +599,7 @@ class Fido2Lib { * @returns {Promise} The options for creating calling `navigator.credentials.create()` */ async attestationOptions(opts) { + await ready; opts = opts || {}; // The object being returned is described here: @@ -714,6 +720,7 @@ class Fido2Lib { * @returns {Promise} The options to be passed to `navigator.credentials.get()` */ async assertionOptions(opts) { + await ready; opts = opts || {}; // https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptions diff --git a/lib/toolbox.js b/lib/toolbox.js index d1411486..060ad839 100644 --- a/lib/toolbox.js +++ b/lib/toolbox.js @@ -18,24 +18,41 @@ import base64 from "@hexagon/base64"; import { Certificate } from "./certUtils.js"; import { PublicKey } from "./keyUtils.js"; + +function isNode() { + // see: https://stackoverflow.com/a/35813135 + // and https://github.com/contentful/contentful.js/issues/422#issuecomment-1054400365 + return typeof process !== "undefined" && + !process.browser && + !!process.versions && + !!process.versions.node; +} + // Import webcrypto -import * as platformCrypto from "crypto"; -import * as peculiarCrypto from "@peculiar/webcrypto"; let webcrypto; -if ((typeof self !== "undefined") && "crypto" in self) { - // Always use crypto if available natively (browser / Deno) - webcrypto = self.crypto; - -} else { - // Always use node webcrypto if available ( >= 16.0 ) - if(platformCrypto && platformCrypto.webcrypto) { - webcrypto = platformCrypto.webcrypto; +const __setWebCrypto = async () => { + if (isNode()) { + // Always use node webcrypto if available ( >= 16.0 ) + return import("crypto").then(m => { + webcrypto = m.webcrypto; + }).catch(() => { + // Fallback to @peculiar/webcrypto + return import("@peculiar/webcrypto").then(m => { + webcrypto = new m.Crypto(); + }); + }); + } + if ((typeof self !== "undefined") && "crypto" in self) { + // Always use crypto if available natively (browser / Deno) + webcrypto = self.crypto; + return; + } + throw new Error("WebCrypto implementation not available. " + + "Install optional dependecies to include @peculiar/webcrypto or " + + "update your Node.js (later than 16)."); +}; +let ready = __setWebCrypto(); - } else { - // Fallback to @peculiar/webcrypto - webcrypto = new peculiarCrypto.Crypto(); - } -} // Set up pkijs const pkijs = { @@ -46,15 +63,17 @@ const pkijs = { CertificateChainValidationEngine, PublicKeyInfo, }; -pkijs.setEngine( - "newEngine", - webcrypto, - new pkijs.CryptoEngine({ - name: "", - crypto: webcrypto, - subtle: webcrypto.subtle, - }), -); +ready = ready.then(() => { + pkijs.setEngine( + "newEngine", + webcrypto, + new pkijs.CryptoEngine({ + name: "", + crypto: webcrypto, + subtle: webcrypto.subtle, + }), + ); +}); function extractBigNum(fullArray, start, end, expectedLength) { let num = fullArray.slice(start, end); @@ -363,5 +382,6 @@ export { pkijs, randomValues, verifySignature, - webcrypto + webcrypto, + ready }; From ae6afbff30cd5f20bc1c347e05c39bb36e33c079 Mon Sep 17 00:00:00 2001 From: Yuval S Date: Sun, 17 Dec 2023 11:45:19 +0200 Subject: [PATCH 2/7] mark @peculiar/webcrypto dependency as optional It's only required for node engines <16, which are no longer under Node.js maintenance. --- package-lock.json | 24 +++++++++++++++++------- package.json | 4 +++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index fbfd35e7..66f13c21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,15 @@ { "name": "fido2-lib", - "version": "3.4.1", + "version": "3.4.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fido2-lib", - "version": "3.4.1", + "version": "3.4.3", "license": "MIT", "dependencies": { "@hexagon/base64": "~1.1.26", - "@peculiar/webcrypto": "~1.4.3", "asn1js": "~3.0.2", "cbor-x": "~1.5.3", "jose": "^4.14.4", @@ -31,6 +30,9 @@ }, "engines": { "node": ">=10" + }, + "optionalDependencies": { + "@peculiar/webcrypto": "~1.4.3" } }, "node_modules/@babel/parser": { @@ -51,7 +53,7 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { +"node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz", "integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==", @@ -320,6 +322,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", + "optional": true, "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.2", @@ -330,6 +333,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "optional": true, "dependencies": { "tslib": "^2.0.0" }, @@ -341,6 +345,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz", "integrity": "sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==", + "optional": true, "dependencies": { "@peculiar/asn1-schema": "^2.3.6", "@peculiar/json-schema": "^1.1.12", @@ -1357,7 +1362,7 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { +"node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", @@ -2946,6 +2951,7 @@ "version": "1.7.7", "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", + "optional": true, "dependencies": { "@peculiar/asn1-schema": "^2.3.6", "@peculiar/json-schema": "^1.1.12", @@ -3106,7 +3112,7 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cbor-extract/cbor-extract-darwin-arm64": { +"@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz", "integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==", @@ -3292,6 +3298,7 @@ "version": "2.3.6", "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", + "optional": true, "requires": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.2", @@ -3302,6 +3309,7 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "optional": true, "requires": { "tslib": "^2.0.0" } @@ -3310,6 +3318,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz", "integrity": "sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==", + "optional": true, "requires": { "@peculiar/asn1-schema": "^2.3.6", "@peculiar/json-schema": "^1.1.12", @@ -4082,7 +4091,7 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "fsevents": { +"fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", @@ -5254,6 +5263,7 @@ "version": "1.7.7", "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", + "optional": true, "requires": { "@peculiar/asn1-schema": "^2.3.6", "@peculiar/json-schema": "^1.1.12", diff --git a/package.json b/package.json index 22feb223..26e2d688 100644 --- a/package.json +++ b/package.json @@ -49,13 +49,15 @@ }, "dependencies": { "@hexagon/base64": "~1.1.26", - "@peculiar/webcrypto": "~1.4.3", "asn1js": "~3.0.2", "cbor-x": "~1.5.3", "jose": "^4.14.4", "pkijs": "~3.0.15", "tldts": "~6.0.5" }, + "optionalDependencies": { + "@peculiar/webcrypto": "~1.4.3" + }, "eslintConfig": { "root": true, "env": { From e5eb8c4fac2ba01c1d1f4de5da154a398f5ff927 Mon Sep 17 00:00:00 2001 From: Yuval S Date: Sun, 17 Dec 2023 11:58:13 +0200 Subject: [PATCH 3/7] fix: ambient type import While for some reason it works when installing from the online repo, it fails when installing the package from the filesystem. See https://stackoverflow.com/questions/39040108 This is poorly documented, but also see: - https://www.typescriptlang.org/docs/handbook/modules/reference.html#ambient-modules - https://www.typescriptlang.org/docs/handbook/modules/reference.html#import-types --- types/main.d.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/types/main.d.ts b/types/main.d.ts index 2f95ead8..16c93a7f 100644 --- a/types/main.d.ts +++ b/types/main.d.ts @@ -1,8 +1,9 @@ /// -import {JWTPayload} from "jose/dist/types/types"; - declare module "fido2-lib" { + // Type imports in ambient module should use import(), + // see https://stackoverflow.com/questions/39040108 + type JWTPayload = import("jose/dist/types/types").JWTPayload; class MdsEntry{ constructor(mdsEntry: Object, tocEntry: Object) From cca9547597320bb2e4ad183ca674512e0c967cd2 Mon Sep 17 00:00:00 2001 From: Yuval Sadan Date: Sun, 17 Dec 2023 15:57:56 +0200 Subject: [PATCH 4/7] fix: more percise error message --- lib/toolbox.js | 6 +++--- package-lock.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/toolbox.js b/lib/toolbox.js index 060ad839..d8b849f0 100644 --- a/lib/toolbox.js +++ b/lib/toolbox.js @@ -47,9 +47,9 @@ const __setWebCrypto = async () => { webcrypto = self.crypto; return; } - throw new Error("WebCrypto implementation not available. " + - "Install optional dependecies to include @peculiar/webcrypto or " + - "update your Node.js (later than 16)."); + throw new Error("Environment is neither Node nor WebCrypto-enabled " + + "browser; no crypto support. If running on an older browser, consider " + + "using a WebCrypto polyfill."); }; let ready = __setWebCrypto(); diff --git a/package-lock.json b/package-lock.json index 66f13c21..1085c719 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, -"node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz", "integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==", @@ -1362,7 +1362,7 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, -"node_modules/fsevents": { + "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", @@ -3112,7 +3112,7 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, -"@cbor-extract/cbor-extract-darwin-arm64": { + "@cbor-extract/cbor-extract-darwin-arm64": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.1.1.tgz", "integrity": "sha512-blVBy5MXz6m36Vx0DfLd7PChOQKEs8lK2bD1WJn/vVgG4FXZiZmZb2GECHFvVPA5T7OnODd9xZiL3nMCv6QUhA==", @@ -4091,7 +4091,7 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, -"fsevents": { + "fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", From 1b9ea80d1db0d065273bfeeeacb455ea6fc4da1a Mon Sep 17 00:00:00 2001 From: Yuval Sadan Date: Sun, 17 Dec 2023 16:07:47 +0200 Subject: [PATCH 5/7] fix: default to polyfill --- lib/toolbox.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/toolbox.js b/lib/toolbox.js index d8b849f0..02229877 100644 --- a/lib/toolbox.js +++ b/lib/toolbox.js @@ -31,25 +31,25 @@ function isNode() { // Import webcrypto let webcrypto; const __setWebCrypto = async () => { + const usePolyfill = async () => { + return import("@peculiar/webcrypto").then(m => { + webcrypto = new m.Crypto(); + }); + } + if (isNode()) { // Always use node webcrypto if available ( >= 16.0 ) return import("crypto").then(m => { webcrypto = m.webcrypto; - }).catch(() => { - // Fallback to @peculiar/webcrypto - return import("@peculiar/webcrypto").then(m => { - webcrypto = new m.Crypto(); - }); - }); + }).catch(usePolyfill); } if ((typeof self !== "undefined") && "crypto" in self) { // Always use crypto if available natively (browser / Deno) webcrypto = self.crypto; return; } - throw new Error("Environment is neither Node nor WebCrypto-enabled " + - "browser; no crypto support. If running on an older browser, consider " + - "using a WebCrypto polyfill."); + // Some environment without WebCrypto; use a polyfill + return usePolyfill(); }; let ready = __setWebCrypto(); From 7fb925e3f32d48ab956f0f2d3fc313505663362a Mon Sep 17 00:00:00 2001 From: Yuval Sadan Date: Sun, 17 Dec 2023 16:19:18 +0200 Subject: [PATCH 6/7] fix: ready immediately if possible --- lib/toolbox.js | 51 ++++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/lib/toolbox.js b/lib/toolbox.js index 02229877..926ea909 100644 --- a/lib/toolbox.js +++ b/lib/toolbox.js @@ -29,29 +29,26 @@ function isNode() { } // Import webcrypto -let webcrypto; -const __setWebCrypto = async () => { - const usePolyfill = async () => { - return import("@peculiar/webcrypto").then(m => { - webcrypto = new m.Crypto(); - }); - } - - if (isNode()) { - // Always use node webcrypto if available ( >= 16.0 ) - return import("crypto").then(m => { - webcrypto = m.webcrypto; - }).catch(usePolyfill); - } - if ((typeof self !== "undefined") && "crypto" in self) { - // Always use crypto if available natively (browser / Deno) - webcrypto = self.crypto; - return; - } - // Some environment without WebCrypto; use a polyfill - return usePolyfill(); +let webcrypto = undefined; +if ((typeof self !== "undefined") && ("crypto" in self)) { + // Always use crypto if available natively (browser / Deno) + webcrypto = self.crypto; +} +const usePolyfill = async () => { + const module = await import("@peculiar/webcrypto"); + webcrypto = new module.Crypto(); +}; +const useNative = async () => { + // Always use node webcrypto if available ( Node >= 16.0 ) + // This also allows bundlers to resolve crypto with a custom polyfill + const module = await import("crypto"); + webcrypto = module.webcrypto; }; -let ready = __setWebCrypto(); + +let ready = Promise.resolve(); +if (webcrypto === undefined) { + ready = ready.then(useNative).catch(usePolyfill); +} // Set up pkijs @@ -63,7 +60,7 @@ const pkijs = { CertificateChainValidationEngine, PublicKeyInfo, }; -ready = ready.then(() => { +const setUpPkijs = () => { pkijs.setEngine( "newEngine", webcrypto, @@ -73,7 +70,13 @@ ready = ready.then(() => { subtle: webcrypto.subtle, }), ); -}); +}; +if (webcrypto === undefined) { + ready = ready.then(setUpPkijs); +} +else { + setUpPkijs(); +} function extractBigNum(fullArray, start, end, expectedLength) { let num = fullArray.slice(start, end); From 32a8e0db90ae672509a460e71817ce3d3fa7b3b2 Mon Sep 17 00:00:00 2001 From: Yuval Sadan Date: Thu, 21 Dec 2023 18:39:53 +0200 Subject: [PATCH 7/7] fix: remove unused isNode --- lib/toolbox.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/toolbox.js b/lib/toolbox.js index 926ea909..132e0665 100644 --- a/lib/toolbox.js +++ b/lib/toolbox.js @@ -19,15 +19,6 @@ import { Certificate } from "./certUtils.js"; import { PublicKey } from "./keyUtils.js"; -function isNode() { - // see: https://stackoverflow.com/a/35813135 - // and https://github.com/contentful/contentful.js/issues/422#issuecomment-1054400365 - return typeof process !== "undefined" && - !process.browser && - !!process.versions && - !!process.versions.node; -} - // Import webcrypto let webcrypto = undefined; if ((typeof self !== "undefined") && ("crypto" in self)) {