From 328cdee75cf6931a760ffa18044dbdf1991b3068 Mon Sep 17 00:00:00 2001 From: Gancho Radkov Date: Tue, 20 Aug 2024 16:22:21 +0300 Subject: [PATCH] chore: reverts verify v2 --- package-lock.json | 137 -------- package.json | 1 - packages/core/src/constants/verify.ts | 1 - packages/core/src/controllers/verify.ts | 309 ++++++------------ packages/core/src/core.ts | 2 +- packages/sign-client/src/client.ts | 1 + .../sign-client/src/controllers/engine.ts | 66 ++-- packages/types/src/core/verify.ts | 14 +- packages/utils/package.json | 2 - packages/utils/src/crypto.ts | 66 ---- packages/utils/test/crypto.spec.ts | 90 ----- rollup.config.js | 2 - 12 files changed, 138 insertions(+), 553 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30eeabeff..267fb2489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,6 @@ ], "devDependencies": { "@rollup/plugin-commonjs": "22.0.2", - "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "13.3.0", "@types/node": "18.7.3", "@types/sinon": "10.0.13", @@ -6373,54 +6372,6 @@ "rollup": "^2.68.0" } }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-json/node_modules/@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-json/node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - }, "node_modules/@rollup/plugin-node-resolve": { "version": "13.3.0", "dev": true, @@ -7470,15 +7421,6 @@ "@types/chai": "*" } }, - "node_modules/@types/elliptic": { - "version": "6.4.18", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.18.tgz", - "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", - "dev": true, - "dependencies": { - "@types/bn.js": "*" - } - }, "node_modules/@types/estree": { "version": "0.0.39", "dev": true, @@ -27566,34 +27508,13 @@ "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "detect-browser": "5.3.0", - "elliptic": "^6.5.7", "query-string": "7.1.3", "uint8arrays": "3.1.0" }, "devDependencies": { - "@types/elliptic": "^6.4.18", "@types/lodash.isequal": "4.5.6" } }, - "packages/utils/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "packages/utils/node_modules/elliptic": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", - "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "packages/utils/node_modules/uint8arrays": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", @@ -32516,34 +32437,6 @@ "resolve": "^1.17.0" } }, - "@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "requires": { - "@rollup/pluginutils": "^5.1.0" - }, - "dependencies": { - "@rollup/pluginutils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", - "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", - "dev": true, - "requires": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^2.3.1" - } - }, - "@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true - } - } - }, "@rollup/plugin-node-resolve": { "version": "13.3.0", "dev": true, @@ -33395,15 +33288,6 @@ "@types/chai": "*" } }, - "@types/elliptic": { - "version": "6.4.18", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.18.tgz", - "integrity": "sha512-UseG6H5vjRiNpQvrhy4VF/JXdA3V/Fp5amvveaL+fs28BZ6xIKJBPnUPRlEaZpysD9MbpfaLi8lbl7PGUAkpWw==", - "dev": true, - "requires": { - "@types/bn.js": "*" - } - }, "@types/estree": { "version": "0.0.39", "dev": true @@ -34151,7 +34035,6 @@ "@stablelib/random": "1.0.2", "@stablelib/sha256": "1.0.1", "@stablelib/x25519": "1.0.3", - "@types/elliptic": "^6.4.18", "@types/lodash.isequal": "4.5.6", "@walletconnect/relay-api": "1.0.11", "@walletconnect/safe-json": "1.0.2", @@ -34160,30 +34043,10 @@ "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "detect-browser": "5.3.0", - "elliptic": "^6.5.7", "query-string": "7.1.3", "uint8arrays": "3.1.0" }, "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - }, - "elliptic": { - "version": "6.5.7", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", - "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, "uint8arrays": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz", diff --git a/package.json b/package.json index 658f4d83e..0d53dd87c 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ }, "devDependencies": { "@rollup/plugin-commonjs": "22.0.2", - "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "13.3.0", "@types/node": "18.7.3", "@types/sinon": "10.0.13", diff --git a/packages/core/src/constants/verify.ts b/packages/core/src/constants/verify.ts index 77049537a..479fe969d 100644 --- a/packages/core/src/constants/verify.ts +++ b/packages/core/src/constants/verify.ts @@ -3,6 +3,5 @@ export const VERIFY_CONTEXT = "verify-api"; const VERIFY_SERVER_COM = "https://verify.walletconnect.com"; const VERIFY_SERVER_ORG = "https://verify.walletconnect.org"; export const VERIFY_SERVER = VERIFY_SERVER_ORG; -export const VERIFY_SERVER_V2 = `${VERIFY_SERVER}/v2`; export const TRUSTED_VERIFY_URLS = [VERIFY_SERVER_COM, VERIFY_SERVER_ORG]; diff --git a/packages/core/src/controllers/verify.ts b/packages/core/src/controllers/verify.ts index 925a47395..a710b0e2b 100644 --- a/packages/core/src/controllers/verify.ts +++ b/packages/core/src/controllers/verify.ts @@ -1,135 +1,68 @@ import { generateChildLogger, getLoggerContext, Logger } from "@walletconnect/logger"; -import { ICore, IVerify } from "@walletconnect/types"; -import { isBrowser, isNode, P256KeyDataType, verifyP256Jwt } from "@walletconnect/utils"; +import { IVerify } from "@walletconnect/types"; +import { isBrowser, isNode, isReactNative } from "@walletconnect/utils"; import { FIVE_SECONDS, ONE_SECOND, toMiliseconds } from "@walletconnect/time"; -import { getDocument } from "@walletconnect/window-getters"; - -import { - CORE_STORAGE_PREFIX, - CORE_VERSION, - TRUSTED_VERIFY_URLS, - VERIFY_CONTEXT, - VERIFY_SERVER, - VERIFY_SERVER_V2, -} from "../constants"; -import { IKeyValueStorage } from "@walletconnect/keyvaluestorage"; - -type Jwk = { - publicKey: P256KeyDataType; - expiresAt: number; -}; -type JwkPayload = { - exp: number; - id: string; - origin: string; - isScam: boolean; - isVerified: boolean; -}; + +import { TRUSTED_VERIFY_URLS, VERIFY_CONTEXT, VERIFY_SERVER } from "../constants"; + export class Verify extends IVerify { public name = VERIFY_CONTEXT; + private verifyUrl: string; + private iframe?: HTMLIFrameElement; + private initialized = false; private abortController: AbortController; private isDevEnv; - private verifyUrlV2 = VERIFY_SERVER_V2; - private storagePrefix = CORE_STORAGE_PREFIX; - private version = CORE_VERSION; - private publicKey?: Jwk; - private fetchPromise?: Promise; - - constructor(public core: ICore, public logger: Logger, public store: IKeyValueStorage) { - super(core, logger, store); + // the queue is only used during the loading phase of the iframe to ensure all attestations are posted + private queue: string[] = []; + // flag to disable verify when the iframe fails to load on main & fallback urls. + // this means Verify API is not enabled for the current projectId and there's no point in trying to initialize it again. + private verifyDisabled = false; + + constructor(public projectId: string, public logger: Logger) { + super(projectId, logger); this.logger = generateChildLogger(logger, this.name); + this.verifyUrl = VERIFY_SERVER; this.abortController = new AbortController(); this.isDevEnv = isNode() && process.env.IS_VITEST; - this.init(); } - get storeKey(): string { - return ( - this.storagePrefix + this.version + this.core.customStoragePrefix + "//" + `verify:public:key` - ); - } + public init: IVerify["init"] = async (params) => { + if (this.verifyDisabled) return; + + // ignore on non browser environments + if (isReactNative() || !isBrowser()) return; - public init = async () => { - this.publicKey = await this.store.getItem(this.storeKey); - if (this.publicKey && toMiliseconds(this.publicKey?.expiresAt) < Date.now()) { - this.logger.debug("verify v2 public key expired"); - await this.removePublicKey(); + const verifyUrl = this.getVerifyUrl(params?.verifyUrl); + // if init is called again with a different url, remove the iframe and start over + if (this.verifyUrl !== verifyUrl) { + this.removeIframe(); } - if (!this.publicKey) { - await this.fetchAndPersistPublicKey(); + this.verifyUrl = verifyUrl; + + try { + await this.createIframe(); + } catch (error) { + this.logger.info(`Verify iframe failed to load: ${this.verifyUrl}`); + this.logger.info(error); + // if the iframe fails to load, disable verify + this.verifyDisabled = true; } }; public register: IVerify["register"] = async (params) => { - if (!isBrowser()) return; - const { id, decryptedId } = params; - const url = `${this.verifyUrlV2}/attestation?projectId=${this.core.projectId}`; - let src = ""; - try { - const response = await fetch(url, { - method: "POST", - body: JSON.stringify({ id, decryptedId }), - }); - const { srcdoc } = await response.json(); - src = srcdoc; - this.logger.debug("srcdoc fetched", src); - } catch (e) { - this.logger.warn(e); - return; + if (!this.initialized) { + this.addToQueue(params.attestationId); + await this.init(); + } else { + this.sendPost(params.attestationId); } - try { - const document = getDocument() as Document; - const abortTimeout = this.startAbortTimer(ONE_SECOND * 3); - const attestationJwt = await new Promise((resolve) => { - const abortListener = () => { - window.removeEventListener("message", listener); - document.body.removeChild(iframe); - throw new Error("attestation aborted"); - }; - this.abortController.signal.addEventListener("abort", abortListener, { - signal: this.abortController.signal, - }); - const iframe = document.createElement("iframe"); - iframe.srcdoc = src; - iframe.style.display = "none"; - const listener = (event: MessageEvent) => { - if (!event.data) return; - const data = JSON.parse(event.data); - if (data.type === "verify_attestation") { - clearInterval(abortTimeout); - document.body.removeChild(iframe); - this.abortController.signal.removeEventListener("abort", abortListener); - window.removeEventListener("message", listener); - resolve(data.attestation === null ? "" : data.attestation); - } - }; - document.body.appendChild(iframe); - window.addEventListener("message", listener, { signal: this.abortController.signal }); - }); - this.logger.debug("jwt attestation", attestationJwt); - return attestationJwt as string; - } catch (e) { - this.logger.warn(e); - } - return ""; }; public resolve: IVerify["resolve"] = async (params) => { if (this.isDevEnv) return ""; - const { attestationId, hash } = params; - if (attestationId === "") { - this.logger.debug("resolve: attestationId is empty, skipping"); - return; - } - - if (attestationId) { - const validation = await this.isValidJwtAttestation(attestationId); - if (validation) return validation; - } - if (!hash) return; const verifyUrl = this.getVerifyUrl(params?.verifyUrl); - return this.fetchAttestation(hash, verifyUrl); + return this.fetchAttestation(params.attestationId, verifyUrl); }; get context(): string { @@ -137,119 +70,95 @@ export class Verify extends IVerify { } private fetchAttestation = async (attestationId: string, url: string) => { - this.logger.debug(`resolving attestation: ${attestationId} from url: ${url}`); + this.logger.info(`resolving attestation: ${attestationId} from url: ${url}`); // set artificial timeout to prevent hanging const timeout = this.startAbortTimer(ONE_SECOND * 5); - const result = await fetch(`${url}/attestation/${attestationId}?v2Supported=true`, { + const result = await fetch(`${url}/attestation/${attestationId}`, { signal: this.abortController.signal, }); clearTimeout(timeout); return result.status === 200 ? await result.json() : undefined; }; - private startAbortTimer(timer: number) { - this.abortController = new AbortController(); - return setTimeout(() => this.abortController.abort(), toMiliseconds(timer)); - } - - private getVerifyUrl = (verifyUrl?: string) => { - let url = verifyUrl || VERIFY_SERVER; - if (!TRUSTED_VERIFY_URLS.includes(url)) { - this.logger.info( - `verify url: ${url}, not included in trusted list, assigning default: ${VERIFY_SERVER}`, - ); - url = VERIFY_SERVER; - } - return url; + private addToQueue = (attestationId: string) => { + this.queue.push(attestationId); }; - private fetchPublicKey = async () => { - try { - this.logger.debug(`fetching public key from: ${this.verifyUrlV2}`); - const timeout = this.startAbortTimer(FIVE_SECONDS); - const result = await fetch(`${this.verifyUrlV2}/public-key`, { - signal: this.abortController.signal, - }); - clearTimeout(timeout); - return (await result.json()) as Jwk; - } catch (e) { - this.logger.warn(e); - } - return undefined; + private processQueue = () => { + if (this.queue.length === 0) return; + this.queue.forEach((attestationId) => this.sendPost(attestationId)); + this.queue = []; }; - private persistPublicKey = async (publicKey: Jwk) => { - this.logger.debug(`persisting public key to local storage`, publicKey); - await this.store.setItem(this.storeKey, publicKey); - this.publicKey = publicKey; - }; - - private removePublicKey = async () => { - this.logger.debug(`removing verify v2 public key from storage`); - await this.store.removeItem(this.storeKey); - this.publicKey = undefined; + private sendPost = (attestationId: string) => { + try { + if (!this.iframe) return; + this.iframe.contentWindow?.postMessage(attestationId, "*"); // setting targetOrigin to "*" fixes the `Failed to execute 'postMessage' on 'DOMWindow': The target origin provided...` while the iframe is still loading + this.logger.info(`postMessage sent: ${attestationId} ${this.verifyUrl}`); + } catch (e) {} }; - private isValidJwtAttestation = async (attestation: string) => { - const key = await this.getPublicKey(); - try { - if (key) { - const validation = this.validateAttestation(attestation, key); - return validation; + private createIframe = async () => { + let iframeOnLoadResolve: () => void; + const onMessage = (event: MessageEvent) => { + if (event.data === "verify_ready") { + this.onInit(); + window.removeEventListener("message", onMessage); + iframeOnLoadResolve(); } - } catch (e) { - this.logger.error(e); - this.logger.warn("error validating attestation"); - } - const newKey = await this.fetchAndPersistPublicKey(); - try { - if (newKey) { - const validation = this.validateAttestation(attestation, newKey); - return validation; - } - } catch (e) { - this.logger.error(e); - this.logger.warn("error validating attestation"); - } - return undefined; + }; + await Promise.race([ + new Promise((resolve) => { + const existingIframe = document.getElementById(VERIFY_CONTEXT); + if (existingIframe) { + this.iframe = existingIframe as HTMLIFrameElement; + this.onInit(); + return resolve(); + } + + window.addEventListener("message", onMessage); + const iframe = document.createElement("iframe"); + iframe.id = VERIFY_CONTEXT; + iframe.src = `${this.verifyUrl}/${this.projectId}`; + iframe.style.display = "none"; + document.body.append(iframe); + this.iframe = iframe; + iframeOnLoadResolve = resolve; + }), + new Promise((_, reject) => + setTimeout(() => { + window.removeEventListener("message", onMessage); + reject("verify iframe load timeout"); + }, toMiliseconds(FIVE_SECONDS)), + ), + ]); }; - private getPublicKey = async () => { - if (this.publicKey) return this.publicKey; - return await this.fetchAndPersistPublicKey(); + private onInit = () => { + this.initialized = true; + this.processQueue(); }; - private fetchAndPersistPublicKey = async () => { - if (this.fetchPromise) { - await this.fetchPromise; - return this.publicKey; - } - this.fetchPromise = new Promise(async (resolve) => { - const key = await this.fetchPublicKey(); - if (!key) return; - await this.persistPublicKey(key); - resolve(key); - }); - const key = await this.fetchPromise; - this.fetchPromise = undefined; - return key; - }; + private startAbortTimer(timer: number) { + this.abortController = new AbortController(); + return setTimeout(() => this.abortController.abort(), toMiliseconds(timer)); + } - private validateAttestation = (attestation: string, key: Jwk) => { - const result = verifyP256Jwt(attestation, key.publicKey); - const validation = { - hasExpired: toMiliseconds(result.exp) < Date.now(), - payload: result, - }; + private removeIframe = () => { + if (!this.iframe) return; + this.iframe.remove(); + this.iframe = undefined; + this.initialized = false; + }; - if (validation.hasExpired) { - this.logger.warn("resolve: jwt attestation expired"); - throw new Error("JWT attestation expired"); + private getVerifyUrl = (verifyUrl?: string) => { + let url = verifyUrl || VERIFY_SERVER; + if (!TRUSTED_VERIFY_URLS.includes(url)) { + this.logger.info( + `verify url: ${url}, not included in trusted list, assigning default: ${VERIFY_SERVER}`, + ); + url = VERIFY_SERVER; } - - return { - origin: validation.payload.origin, - isScam: validation.payload.isScam, - }; + return url; }; } diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 1afc9cffa..a676a7c75 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -108,7 +108,7 @@ export class Core extends ICore { projectId: this.projectId, }); this.pairing = new Pairing(this, this.logger); - this.verify = new Verify(this, this.logger, this.storage); + this.verify = new Verify(this.projectId || "", this.logger); this.echoClient = new EchoClient(this.projectId || "", this.logger); } diff --git a/packages/sign-client/src/client.ts b/packages/sign-client/src/client.ts index fb6427f6a..02f46dda7 100644 --- a/packages/sign-client/src/client.ts +++ b/packages/sign-client/src/client.ts @@ -251,6 +251,7 @@ export class SignClient extends ISignClient { await this.pendingRequest.init(); await this.engine.init(); await this.auth.init(); + this.core.verify.init({ verifyUrl: this.metadata.verifyUrl }); this.logger.info(`SignClient Initialization Success`); this.engine.processRelayMessageCache(); } catch (error: any) { diff --git a/packages/sign-client/src/controllers/engine.ts b/packages/sign-client/src/controllers/engine.ts index 862304f3e..3974ffb6a 100644 --- a/packages/sign-client/src/controllers/engine.ts +++ b/packages/sign-client/src/controllers/engine.ts @@ -81,6 +81,7 @@ import { validateSignedCacao, getNamespacedDidChainId, parseChainId, + isBrowser, } from "@walletconnect/utils"; import EventEmmiter from "events"; import { @@ -1180,7 +1181,10 @@ export class Engine extends IEngine { private sendRequest: EnginePrivate["sendRequest"] = async (args) => { const { topic, method, params, expiry, relayRpcId, clientRpcId, throwOnFailedPublish } = args; const payload = formatJsonRpcRequest(method, params, clientRpcId); - + if (isBrowser() && METHODS_TO_VERIFY.includes(method)) { + const hash = hashMessage(JSON.stringify(payload)); + this.client.core.verify.register({ attestationId: hash }); + } let message; try { message = await this.client.core.crypto.encode(topic, payload); @@ -1190,14 +1194,7 @@ export class Engine extends IEngine { throw error; } - let attestation: string | undefined; - if (METHODS_TO_VERIFY.includes(method)) { - const decryptedId = hashMessage(JSON.stringify(payload)); - const id = hashMessage(message); - attestation = await this.client.core.verify.register({ id, decryptedId }); - } const opts = ENGINE_RPC_OPTS[method].req; - opts.attestation = attestation; if (expiry) opts.ttl = expiry; if (relayRpcId) opts.id = relayRpcId; this.client.core.history.set(topic, payload); @@ -1370,7 +1367,7 @@ export class Engine extends IEngine { }; private processRequest: EnginePrivate["onRelayEventRequest"] = async (event) => { - const { topic, payload, attestation } = event; + const { topic, payload } = event; const reqMethod = payload.method as JsonRpcTypes.WcMethod; if (this.shouldIgnorePairingRequest({ topic, requestMethod: reqMethod })) { @@ -1379,7 +1376,7 @@ export class Engine extends IEngine { switch (reqMethod) { case "wc_sessionPropose": - return await this.onSessionProposeRequest(topic, payload, attestation); + return await this.onSessionProposeRequest(topic, payload); case "wc_sessionSettle": return await this.onSessionSettleRequest(topic, payload); case "wc_sessionUpdate": @@ -1391,11 +1388,11 @@ export class Engine extends IEngine { case "wc_sessionDelete": return await this.onSessionDeleteRequest(topic, payload); case "wc_sessionRequest": - return await this.onSessionRequest(topic, payload, attestation); + return await this.onSessionRequest(topic, payload); case "wc_sessionEvent": return await this.onSessionEventRequest(topic, payload); case "wc_sessionAuthenticate": - return await this.onSessionAuthenticateRequest(topic, payload, attestation); + return await this.onSessionAuthenticateRequest(topic, payload); default: return this.client.logger.info(`Unsupported request method ${reqMethod}`); } @@ -1458,7 +1455,6 @@ export class Engine extends IEngine { private onSessionProposeRequest: EnginePrivate["onSessionProposeRequest"] = async ( topic, payload, - attestation, ) => { const { params, id } = payload; try { @@ -1467,11 +1463,8 @@ export class Engine extends IEngine { params.expiryTimestamp || calcExpiry(ENGINE_RPC_OPTS.wc_sessionPropose.req.ttl); const proposal = { id, pairingTopic: topic, expiryTimestamp, ...params }; await this.setProposal(id, proposal); - const verifyContext = await this.getVerifyContext({ - attestationId: attestation, - hash: hashMessage(JSON.stringify(payload)), - metadata: proposal.proposer.metadata, - }); + const hash = hashMessage(JSON.stringify(payload)); + const verifyContext = await this.getVerifyContext(hash, proposal.proposer.metadata); this.client.events.emit("session_proposal", { id, params: proposal, verifyContext }); } catch (err: any) { await this.sendError({ @@ -1766,20 +1759,15 @@ export class Engine extends IEngine { } }; - private onSessionRequest: EnginePrivate["onSessionRequest"] = async ( - topic, - payload, - attestation, - ) => { + private onSessionRequest: EnginePrivate["onSessionRequest"] = async (topic, payload) => { const { id, params } = payload; try { await this.isValidRequest({ topic, ...params }); + const hash = hashMessage( + JSON.stringify(formatJsonRpcRequest("wc_sessionRequest", params, id)), + ); const session = this.client.session.get(topic); - const verifyContext = await this.getVerifyContext({ - attestationId: attestation, - hash: hashMessage(JSON.stringify(formatJsonRpcRequest("wc_sessionRequest", params, id))), - metadata: session.peer.metadata, - }); + const verifyContext = await this.getVerifyContext(hash, session.peer.metadata); const request = { id, topic, @@ -1871,15 +1859,11 @@ export class Engine extends IEngine { private onSessionAuthenticateRequest: EnginePrivate["onSessionAuthenticateRequest"] = async ( topic, payload, - attestation, ) => { try { const { requester, authPayload, expiryTimestamp } = payload.params; - const verifyContext = await this.getVerifyContext({ - attestationId: attestation, - hash: hashMessage(JSON.stringify(payload)), - metadata: this.client.metadata, - }); + const hash = hashMessage(JSON.stringify(payload)); + const verifyContext = await this.getVerifyContext(hash, this.client.metadata); const pendingRequest = { requester, pairingTopic: topic, @@ -2425,12 +2409,7 @@ export class Engine extends IEngine { } }; - private getVerifyContext = async (params: { - attestationId?: string; - hash?: string; - metadata: CoreTypes.Metadata; - }) => { - const { attestationId, hash, metadata } = params; + private getVerifyContext = async (hash: string, metadata: CoreTypes.Metadata) => { const context: Verify.Context = { verified: { verifyUrl: metadata.verifyUrl || VERIFY_SERVER, @@ -2441,8 +2420,7 @@ export class Engine extends IEngine { try { const result = await this.client.core.verify.resolve({ - attestationId, - hash, + attestationId: hash, verifyUrl: metadata.verifyUrl, }); if (result) { @@ -2452,10 +2430,10 @@ export class Engine extends IEngine { result.origin === new URL(metadata.url).origin ? "VALID" : "INVALID"; } } catch (e) { - this.client.logger.warn(e); + this.client.logger.info(e); } - this.client.logger.debug(`Verify context: ${JSON.stringify(context)}`); + this.client.logger.info(`Verify context: ${JSON.stringify(context)}`); return context; }; diff --git a/packages/types/src/core/verify.ts b/packages/types/src/core/verify.ts index ca13f5380..55a0327de 100644 --- a/packages/types/src/core/verify.ts +++ b/packages/types/src/core/verify.ts @@ -1,6 +1,4 @@ import { Logger } from "@walletconnect/logger"; -import { IKeyValueStorage } from "@walletconnect/keyvaluestorage"; -import { ICore } from "./core"; export declare namespace Verify { export interface Context { @@ -16,16 +14,14 @@ export declare namespace Verify { export abstract class IVerify { public abstract readonly context: string; - constructor(public core: ICore, public logger: Logger, public store: IKeyValueStorage) {} + constructor(public projectId: string, public logger: Logger) {} - public abstract register(params: { - id: string; - decryptedId: string; - }): Promise; + public abstract init(params?: { verifyUrl?: string }): Promise; + + public abstract register(params: { attestationId: string }): Promise; public abstract resolve(params: { - attestationId?: string; - hash?: string; + attestationId: string; verifyUrl?: string; }): Promise<{ origin: string; isScam?: boolean }>; } diff --git a/packages/utils/package.json b/packages/utils/package.json index dd9999c69..0b141dcca 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -43,12 +43,10 @@ "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "detect-browser": "5.3.0", - "elliptic": "^6.5.7", "query-string": "7.1.3", "uint8arrays": "3.1.0" }, "devDependencies": { - "@types/elliptic": "^6.4.18", "@types/lodash.isequal": "4.5.6" } } diff --git a/packages/utils/src/crypto.ts b/packages/utils/src/crypto.ts index 2a5a00098..5c1464f0c 100644 --- a/packages/utils/src/crypto.ts +++ b/packages/utils/src/crypto.ts @@ -5,8 +5,6 @@ import { hash, SHA256 } from "@stablelib/sha256"; import * as x25519 from "@stablelib/x25519"; import { CryptoTypes } from "@walletconnect/types"; import { concat, fromString, toString } from "uint8arrays"; -import { ec as EC } from "elliptic"; -import { decodeJWT } from "@walletconnect/relay-auth"; export const BASE10 = "base10"; export const BASE16 = "base16"; @@ -171,67 +169,3 @@ export function isTypeOneEnvelope( typeof result.receiverPublicKey === "string" ); } - -export function getCryptoKeyFromKeyData(keyData: P256KeyDataType): EC.KeyPair { - const ec = new EC("p256"); - const key = ec.keyFromPublic( - { - x: Buffer.from(keyData.x, "base64").toString("hex"), - y: Buffer.from(keyData.y, "base64").toString("hex"), - }, - "hex", - ); - return key; -} - -function base64UrlToBase64(base64Url: string) { - let base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/"); - const padding = base64.length % 4; - if (padding > 0) { - base64 += "=".repeat(4 - padding); - } - return base64; -} - -function base64UrlDecode(base64Url: string) { - return Buffer.from(base64UrlToBase64(base64Url), "base64"); -} - -export function verifyP256Jwt(token: string, keyData: P256KeyDataType) { - const [headerBase64Url, payloadBase64Url, signatureBase64Url] = token.split("."); - - // Decode the signature - const signatureBuffer = base64UrlDecode(signatureBase64Url); - - // Check if signature length is correct (64 bytes for P-256) - if (signatureBuffer.length !== 64) { - throw new Error("Invalid signature length"); - } - - // Extract r and s from the signature - const r = signatureBuffer.slice(0, 32).toString("hex"); - const s = signatureBuffer.slice(32, 64).toString("hex"); - - // Create the signing input - const signingInput = `${headerBase64Url}.${payloadBase64Url}`; - - const sha256 = new SHA256(); - const buffer = sha256.update(Buffer.from(signingInput)).digest(); - - const key = getCryptoKeyFromKeyData(keyData); - - // Convert the hash to hex format - const hashHex = Buffer.from(buffer).toString("hex"); - - // Verify the signature - const isValid = key.verify(hashHex, { r, s }); - - if (!isValid) { - throw new Error("Invalid signature"); - } - const data = decodeJWT(token) as unknown as { payload: T }; - return { - ...data.payload, - isVerified: isValid, - }; -} diff --git a/packages/utils/test/crypto.spec.ts b/packages/utils/test/crypto.spec.ts index 8b7efeed4..de2e1167a 100644 --- a/packages/utils/test/crypto.spec.ts +++ b/packages/utils/test/crypto.spec.ts @@ -1,9 +1,6 @@ import { expect, describe, it } from "vitest"; import { toString } from "uint8arrays"; import { safeJsonStringify } from "@walletconnect/safe-json"; -import { SHA256 } from "@stablelib/sha256"; -import { Buffer } from "buffer"; -import elliptic from "elliptic"; import { BASE16, @@ -17,9 +14,6 @@ import { validateDecoding, isTypeOneEnvelope, generateRandomBytes32, - verifyP256Jwt, - getCryptoKeyFromKeyData, - P256KeyDataType, } from "../src"; import { TEST_KEY_PAIRS, TEST_SHARED_KEY, TEST_HASHED_KEY, TEST_SYM_KEY } from "./shared"; @@ -114,88 +108,4 @@ describe("Crypto", () => { it("calls generateRandomBytes32", () => { expect(generateRandomBytes32()).toBeTruthy(); }); - it("should validate verify v2 jwt", async () => { - const token = - "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjM2MzI1MDQsImlkIjoiMDkxN2YzMzk0YTdmMzkyZTg3ZTM1ZjM4OTg2OWU2NDEzZjkyNTBlMGIxZTE4YjUzMDhkNzBhM2VjOTJjZDQ3OCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzU2NhbSI6bnVsbCwiaXNWZXJpZmllZCI6ZmFsc2V9.RehA28c0Ae8D_ixvGS8uG9J9eTJtpGfaC_7kNE9ZNAVFREWBY6Dl_SXc0_E0RSvYkHpupfmXlmjenuDqNcyoeg"; - - const publicKey = { - publicKey: { - crv: "P-256", - ext: true, - key_ops: ["verify"], - kty: "EC", - x: "CbL4DOYOb1ntd-8OmExO-oS0DWCMC00DntrymJoB8tk", - y: "KTFwjHtQxGTDR91VsOypcdBfvbo6sAMj5p4Wb-9hRA0", - }, - expiresAt: 1726209328, - }; - - const result = verifyP256Jwt<{ - exp: number; - id: string; - origin: string; - isScam: boolean; - isVerified: true; - }>(token, publicKey.publicKey); - console.log("result", result); - expect(result).to.exist; - expect(result).to.exist; - expect(result.isVerified).to.be.true; - expect(result.exp).to.exist; - expect(result.origin).to.exist; - expect(result.isScam).to.be.null; - await new Promise((resolve) => setTimeout(resolve, 1000)); - }); - it("should fail to validate invalid jwt with public key", async () => { - const token = - "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.oiMDkxN2YzMzk0YTdmMzkyZTg3ZTM1ZjM4OTg2OWU2NDEzZjkyNTBlMGIxZTE4YjUzMDhkNzBhM2VjOTJjZDQ3OCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzU2NhbSI6bnVsbCwiaXNWZXJpZmllZCI6ZmFsc2V9.RehA28c0Ae8D_ixvGS8uG9J9eTJtpGfaC_7kNE9ZNAVFREWBY6Dl_SXc0_E0RSvYkHpupfmXlmjenuDqNcyoeg"; - - const publicKey = { - publicKey: { - crv: "P-256", - ext: true, - key_ops: ["verify"], - kty: "EC", - x: "CbL4DOYOb1ntd-8OmExO-oS0DWCMC00DntrymJoB8tk", - y: "KTFwjHtQxGTDR91VsOypcdBfvbo6sAMj5p4Wb-9hRA0", - }, - expiresAt: 1726209328, - }; - - expect(() => - verifyP256Jwt<{ - exp: number; - id: string; - origin: string; - isScam: boolean; - isVerified: true; - }>(token, publicKey.publicKey), - ).to.throw(); - }); - it("should fail to validate validate verify v2 jwt with invalid public key", async () => { - const token = - "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjM2MzI1MDQsImlkIjoiMDkxN2YzMzk0YTdmMzkyZTg3ZTM1ZjM4OTg2OWU2NDEzZjkyNTBlMGIxZTE4YjUzMDhkNzBhM2VjOTJjZDQ3OCIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImlzU2NhbSI6bnVsbCwiaXNWZXJpZmllZCI6ZmFsc2V9.RehA28c0Ae8D_ixvGS8uG9J9eTJtpGfaC_7kNE9ZNAVFREWBY6Dl_SXc0_E0RSvYkHpupfmXlmjenuDqNcyoeg"; - - const publicKey = { - publicKey: { - crv: "P-256", - ext: true, - key_ops: ["verify"], - kty: "EC", - x: "CbL4DOYOb1ntd-8OmExO-oS0DWCMC00Dn", - y: "KTFwjHtQxGTDR91VsOypcdBfvbo6sAMj5p4Wb-9hRA0", - }, - expiresAt: 1726209328, - }; - - expect(() => - verifyP256Jwt<{ - exp: number; - id: string; - origin: string; - isScam: boolean; - isVerified: true; - }>(token, publicKey.publicKey), - ).to.throw(); - }); }); diff --git a/rollup.config.js b/rollup.config.js index 302e57c6c..83662e0bb 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,12 +1,10 @@ import esbuild from "rollup-plugin-esbuild"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; -import json from "@rollup/plugin-json"; const input = "./src/index.ts"; const plugins = [ nodeResolve({ preferBuiltins: false, browser: true }), - json(), commonjs(), esbuild({ minify: true,