From 45a02f726815bee45ad2094c799219e01fa18d82 Mon Sep 17 00:00:00 2001 From: Jaco Date: Sat, 17 Dec 2022 14:38:32 +0200 Subject: [PATCH] connect() new-style support (#1175) * connect() new-style support * Add connect interface * Seed required * Support both old & new codepaths in inject * Explicit enable(...) reject on new-style * Cleanups * Condense enable(...) error * Adjust enable ordering * Adjust web3IsInjected --- packages/extension-base/src/page/Signer.ts | 2 +- packages/extension-dapp/src/bundle.ts | 117 ++++++++++++--------- packages/extension-inject/src/bundle.ts | 37 +++++-- packages/extension-inject/src/cyrb53.ts | 33 ++++++ packages/extension-inject/src/types.ts | 5 +- 5 files changed, 132 insertions(+), 62 deletions(-) create mode 100644 packages/extension-inject/src/cyrb53.ts diff --git a/packages/extension-base/src/page/Signer.ts b/packages/extension-base/src/page/Signer.ts index 92ce69b5..310b21d7 100644 --- a/packages/extension-base/src/page/Signer.ts +++ b/packages/extension-base/src/page/Signer.ts @@ -38,7 +38,7 @@ export default class Signer implements SignerInterface { } // NOTE We don't listen to updates at all, if we do we can interpret the - // resuklt as provided by the API here + // result as provided by the API here // public update (id: number, status: Hash | SubmittableResult): void { // // ignore // } diff --git a/packages/extension-dapp/src/bundle.ts b/packages/extension-dapp/src/bundle.ts index 45942b58..bfedc245 100644 --- a/packages/extension-dapp/src/bundle.ts +++ b/packages/extension-dapp/src/bundle.ts @@ -1,9 +1,9 @@ // Copyright 2019-2022 @polkadot/extension-dapp authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedProviderWithMeta, InjectedWindow, ProviderList, Unsubcall, Web3AccountsOptions } from '@polkadot/extension-inject/types'; +import type { InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedProviderWithMeta, InjectedWindow, ProviderList, Unsubcall, Web3AccountsOptions } from '@polkadot/extension-inject/types'; -import { u8aEq } from '@polkadot/util'; +import { objectSpread, u8aEq } from '@polkadot/util'; import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'; import { documentReadyPromise } from './util'; @@ -20,7 +20,10 @@ win.injectedWeb3 = win.injectedWeb3 || {}; // true when anything has been injected and is available function web3IsInjected (): boolean { - return Object.keys(win.injectedWeb3).length !== 0; + return Object + .values(win.injectedWeb3) + .filter(({ connect, enable }) => !!(connect || enable)) + .length !== 0; } // helper to throw a consistent error when not enabled @@ -47,18 +50,31 @@ let web3EnablePromise: Promise | null = null; export { isWeb3Injected, web3EnablePromise }; -function getWindowExtensions (originName: string): Promise<[InjectedExtensionInfo, Injected | void][]> { - return Promise.all( - Object.entries(win.injectedWeb3).map( - ([name, { enable, version }]): Promise<[InjectedExtensionInfo, Injected | void]> => - Promise.all([ - Promise.resolve({ name, version }), - enable(originName).catch((error: Error): void => { - console.error(`Error initializing ${name}: ${error.message}`); - }) - ]) +function getWindowExtensions (originName: string): Promise { + return Promise + .all( + Object + .entries(win.injectedWeb3) + .map(([nameOrHash, { connect, enable, version }]): Promise<(InjectedExtension | void)> => + Promise + .resolve() + .then(() => + connect + // new style, returning all info + ? connect(originName) + : enable + // previous interface, leakages on name/version + ? enable(originName).then((e) => + objectSpread({ name: nameOrHash, version: version || 'unknown' }, e) + ) + : Promise.reject(new Error('No connect(..) or enable(...) hook found')) + ) + .catch(({ message }: Error): void => { + console.error(`Error initializing ${nameOrHash}: ${message}`); + }) + ) ) - ); + .then((exts) => exts.filter((e): e is InjectedExtension => !!e)); } // enables all the providers found on the injected window interface @@ -76,29 +92,30 @@ export function web3Enable (originName: string, compatInits: (() => Promise getWindowExtensions(originName) .then((values): InjectedExtension[] => - values - .filter((value): value is [InjectedExtensionInfo, Injected] => !!value[1]) - .map(([info, ext]): InjectedExtension => { - // if we don't have an accounts subscriber, add a single-shot version - if (!ext.accounts.subscribe) { - ext.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise): Unsubcall => { - ext.accounts.get().then(cb).catch(console.error); + values.map((e): InjectedExtension => { + // if we don't have an accounts subscriber, add a single-shot version + if (!e.accounts.subscribe) { + e.accounts.subscribe = (cb: (accounts: InjectedAccount[]) => void | Promise): Unsubcall => { + e.accounts + .get() + .then(cb) + .catch(console.error); - return (): void => { - // no ubsubscribe needed, this is a single-shot - }; + return (): void => { + // no ubsubscribe needed, this is a single-shot }; - } + }; + } - return { ...info, ...ext }; - }) + return e; + }) ) .catch((): InjectedExtension[] => []) .then((values): InjectedExtension[] => { const names = values.map(({ name, version }): string => `${name}/${version}`); isWeb3Injected = web3IsInjected(); - console.log(`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`); + console.info(`web3Enable: Enabled ${values.length} extension${values.length !== 1 ? 's' : ''}: ${names.join(', ')}`); return values; }) @@ -118,27 +135,25 @@ export async function web3Accounts ({ accountType, extensions, ss58Format }: Web const injected = await web3EnablePromise; const retrieved = await Promise.all( - injected.filter( - ({ name: source }) => !extensions || extensions.includes(source) - ).map(async ({ accounts, name: source }): Promise => { - try { - const list = await accounts.get(); + injected + .filter(({ name: source }) => !extensions || extensions.includes(source)) + .map(async ({ accounts, name: source }): Promise => { + try { + const list = await accounts.get(); - return mapAccounts(source, list.filter(({ type }) => type && accountType ? accountType.includes(type) : true), ss58Format); - } catch (error) { - // cannot handle this one - return []; - } - }) + return mapAccounts(source, list.filter(({ type }) => type && accountType ? accountType.includes(type) : true), ss58Format); + } catch (error) { + // cannot handle this one + return []; + } + }) ); retrieved.forEach((result): void => { accounts.push(...result); }); - const addresses = accounts.map(({ address }) => address); - - console.log(`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}: ${addresses.join(', ')}`); + console.info(`web3Accounts: Found ${accounts.length} address${accounts.length !== 1 ? 'es' : ''}`); return accounts; } @@ -152,20 +167,18 @@ export async function web3AccountsSubscribe (cb: (accounts: InjectedAccountWithM const triggerUpdate = (): void | Promise => cb( - Object.entries(accounts).reduce( - (result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => { + Object + .entries(accounts) + .reduce((result: InjectedAccountWithMeta[], [source, list]): InjectedAccountWithMeta[] => { result.push(...mapAccounts(source, list, ss58Format)); return result; - }, - [] - ) + }, []) ); - const unsubs = (await web3EnablePromise).filter( - ({ name: source }) => !extensions || extensions.includes(source) - ).map( - ({ accounts: { subscribe }, name: source }): Unsubcall => + const unsubs = (await web3EnablePromise) + .filter(({ name: source }) => !extensions || extensions.includes(source)) + .map(({ accounts: { subscribe }, name: source }): Unsubcall => subscribe((result): void => { accounts[source] = result; @@ -175,7 +188,7 @@ export async function web3AccountsSubscribe (cb: (accounts: InjectedAccountWithM console.error(error); } }) - ); + ); return (): void => { unsubs.forEach((unsub): void => { diff --git a/packages/extension-inject/src/bundle.ts b/packages/extension-inject/src/bundle.ts index 42b9b27e..7d09bd6a 100644 --- a/packages/extension-inject/src/bundle.ts +++ b/packages/extension-inject/src/bundle.ts @@ -1,10 +1,19 @@ // Copyright 2019-2022 @polkadot/extension-inject authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { Injected, InjectedWindow, InjectOptions } from './types'; +import type { Injected, InjectedExtension, InjectedWindow, InjectOptions } from './types'; + +import { cyrb53 } from './cyrb53'; export { packageInfo } from './packageInfo'; +// setting for new-style connect (more-secure with no details exposed without +// user acknowledgement, however slightly less-compatible with all dapps, some +// may have not upgraded and don't have access to the latest interfaces) +// +// NOTE: In future versions this will be made the default +const IS_CONNECT_CAPABLE = false; + // It is recommended to always use the function below to shield the extension and dapp from // any future changes. The exposed interface will manage access between the 2 environments, // be it via window (current), postMessage (under consideration) or any other mechanism @@ -15,10 +24,24 @@ export function injectExtension (enable: (origin: string) => Promise, // don't clobber the existing object, we will add it (or create as needed) windowInject.injectedWeb3 = windowInject.injectedWeb3 || {}; - // add our enable function - windowInject.injectedWeb3[name] = { - enable: (origin: string): Promise => - enable(origin), - version - }; + if (IS_CONNECT_CAPABLE) { + // expose our extension on the window object, new-style with connect(origin) + windowInject.injectedWeb3[cyrb53(`${name}/${version}`)] = { + connect: (origin: string): Promise => + enable(origin).then(({ accounts, metadata, provider, signer }) => ({ + accounts, metadata, name, provider, signer, version + })), + enable: (): Promise => + Promise.reject( + new Error('This extension does not have support for enable(...), rather is only supports the new connect(...) variant (no extension name/version metadata without specific user-approval)') + ) + }; + } else { + // expose our extension on the window object, old-style with enable(origin) + windowInject.injectedWeb3[name] = { + enable: (origin: string): Promise => + enable(origin), + version + }; + } } diff --git a/packages/extension-inject/src/cyrb53.ts b/packages/extension-inject/src/cyrb53.ts new file mode 100644 index 00000000..9ea1d317 --- /dev/null +++ b/packages/extension-inject/src/cyrb53.ts @@ -0,0 +1,33 @@ +// Copyright 2019-2022 @polkadot/extension-inject authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// cyrb53 (c) 2018 bryc (github.com/bryc) +// A fast and simple hash function with decent collision resistance. +// Largely inspired by MurmurHash2/3, but with a focus on speed/simplicity. +// Public domain. Attribution appreciated. +// +// From https://github.com/bryc/code/blob/fed42df9db547493452e32375c93d7854383e480/jshash/experimental/cyrb53.js +// As shared in https://stackoverflow.com/a/52171480 +// +// Small changes made to the code as linked above: +// . - Seed value is required (set as Date.now() in usage, could change) +// - Return value is a hex string (as per comment in SO answer) +// - TS typings added +// - Non-intrusive coding-style variable declaration changes +export function cyrb53 (input: string, seed = Date.now()): string { + let h1 = 0xdeadbeef ^ seed; + let h2 = 0x41c6ce57 ^ seed; + + for (let i = 0; i < input.length; i++) { + const ch = input.charCodeAt(i); + + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + // https://stackoverflow.com/a/52171480 + return (h2 >>> 0).toString(16).padStart(8, '0') + (h1 >>> 0).toString(16).padStart(8, '0'); +} diff --git a/packages/extension-inject/src/types.ts b/packages/extension-inject/src/types.ts index ed82455d..d37d19b6 100644 --- a/packages/extension-inject/src/types.ts +++ b/packages/extension-inject/src/types.ts @@ -99,8 +99,9 @@ export interface Injected { } export interface InjectedWindowProvider { - enable: (origin: string) => Promise; - version: string; + connect?: (origin: string) => Promise; + enable?: (origin: string) => Promise; + version?: string; } export interface InjectedWindow extends This {