EIP-1102: Opt-in provider access

Hi @anxolin. Thanks for your comments. Responses below:

I think it would be a good idea to also notify the dapp when the user reject the access, so can detect the case and give a better user feedback.

If dapps were notified when a user rejected provider access, that would expose enough information to allow malicious sites to continue uniquely identifying Ethereum users. For example, if a malicious site initiates a provider request, a user rejects access, and the malicious site is notified, the site now knows the user is an Ethereum user within a dapp browser. Even without provider or account access, a malicious site can still run targeted ad and phishing campaigns simply by knowing a certain IP address is associated with Ethereum use.
I agree that this means dapp UX will need to change, and there are discussions around this very topic. Still, the lack of notification of rejection is a crucial piece to this protocol’s privacy.

Also, would it make sense to allow also to request a list of provider details…

This is a very good idea. The request protocol outlined in this proposal was intentionally designed to be open-ended so that future EIPs can build on it and add new information to the request. @danfinlay had some thoughts around this idea as well, such as requesting a specific account type on a specific network when requesting a provider.

Could we just send it as as an argument when the extension triggers the listener?

Because the provider request protocol leverages window.postMessage, it’s not possible to return the provider object as an argument to the listener. Data sent via window.postMessage is serialized using structured cloning which means the provider object’s functions would be lost. Additionally, while some browsers support a third transfer argument when calling window.postMessage to transfer an object between two windows, browser support is very limited. The only viable way to respond with a full provider (for now) is global injection.

Another small detail, why the event called message and then we check for the type ETHEREUM_PROVIDER_SUCCESS? I think the event name can be an arbitrary name, so it can be more specific.

The protocol makes use of window.postMessage to request a provider, and then listens for the browser to post a message using window.postMessage in response. You bring up an interesting point: the dapp browser could respond by triggering an Ethereum-specific event listener, negating the need to sort through other posted messages based on a type. Part of me likes the symmetry of posting a message, then listening for a posted message, but I will experiment with this today for security.

If we chose to emit an event instead of post a response message after user approval as suggested by @anxolin, dapp browsers could return the provider object to the event listener instead of injecting it globally.

We could go from this:

window.addEventListener('message', ({ data }) => {
    if (data && data.type && data.type === 'ETHEREUM_PROVIDER_SUCCESS') {
        const networkVersion = await window.ethereum.send('net_version', []);
    }
});
window.postMessage({ type: 'ETHEREUM_PROVIDER_REQUEST' }, '*');

To something like this:

window.addEventListener('ethereumprovider', (ethereum) => {
    const networkVersion = await ethereum.send('net_version', []);
});
window.postMessage({ type: 'ETHEREUM_PROVIDER_REQUEST' }, '*');

There wouldn’t be any immediate fingerprinting threat since an event would only be emitted after user approval. What do you think? @anxolin @ryanio @danfinlay @andytudhope @serso @brunobar79

3 Likes

Thanks for including me.

Choosing to emit an event sounds nice. However, I found when coding the implementation in Mist that assigning the provider to window.ethereum is actually nice because on page load if the dapp has previously been authenticated by the user we can load the page with window.ethereum already available without needing another request.

(Also in dev environments you could assume window.ethereum is available without needing to request it.)

With sending an event, we wouldn’t know when in the dapp lifecycle the listener would be set up to know when to emit.

Hi @ryanio, thanks for the feedback. I see your point about auto-injecting window.ethereum for already-approved sites, but this seems like an odd approach for approval caching since it requires dapps to check for window.ethereum before requesting a provider. If a domain has already been approved, rather than injecting window.ethereum automatically, why not still wait for a request, but respond immediately with the provider and show no user dialog? For reference, this is what MetaMask is doing. This approach allows domain caching (meaning users don’t have to constantly re-approve the same dapp) while imposing no additional code on dapps (they still request and add a listener like normal and don’t have to first check for window.ethereum.) It would also be trivial to dispatch an ethereumprovider event on the Window for local testing.

I’m starting to think the ethereumprovider event for success response is a cleaner API, but I’m definitely open to more thoughts.

1 Like

hey guys, I’m Andrey , implementing 1102 in status.im.

Agree with @bitpshr, ethereumprovider event looks cleaner, only one way how to get a provider, and safer, because we don’t post a message for success response. We also implemented in status immediate response if the domain was allowed before.

Do you plan to use CustomEvent? so it will be something like

window.addEventListener('ethereumprovider', (event) => {
    const networkVersion = await event.detail.ethereum.send('net_version', []);
});
4 Likes

So, re: The UX of EIP 1102 - looking for a way to guarantee both user control of privacy and preserve the relative ease of UX available with current Metamask/web3.

My general idea to solve this is basically to find a way to automatically push a refresh and injection of window.ethereum based on an application requesting that, upon install of Metamask, their still-open tab will be able to have a pending request of availability somehow notify the user that they’ve requested access, and then upon access being granted, initiate the reload and window.ethereum injection. Doing so would not expose any user information without their request, while still allowing applications to have relatively seamless UX flow (with the workflow, 1. Request Metamask install and/or request acceptance, 2. Detect Metamask has been opened and accepted). The en-queuing of such a request could be easily accomplished by storage of a specially keyed cookie or localStorage object (e.g., _web3_permission_pending_request). Any risk of accidental acceptance of a website’s pending permission requests could be mitigated by a combiation of (a) only showing requests originating from open tabs or domains, and (b) sufficiently clear UX on the part of the ethereum provider (e.g. Metamask). I strongly encourage an approach like this for keeping UX simplicity.

I like how clean this is, and would be in favor of it provided there are no major downsides we can think of.

I prefer this kind of provider consistency even across dev environments, and I hope @ryanio is open to it.

We are currently implementing/testing provider injection of the GnosisSafe extension (safe.gnosis.io) and often have issues if we have both (the MetaMask extension and the GnosisSafe extension) installed.

Since the GnosisSafe extension already uses a whitelisting approach to check if it should inject a web3 instance, we currently just override any pre-existing web3 instance (in that case the MetaMask instance). This is obviously super ugly and therefore we are quite happy about EIP-1102 and id field :smiley:

I think having an event that returns the provider that should be injected is cleaner than just injecting into a global variable. It would be nice to provide the id of the wallet that emitted the ethereumprovider event.

3 Likes

Cross-posting this here for anyone not following the thread relating to UX around this proposal:

Has there been any discussion about simply using domain whitelisting without user requests and, say, having the Web3 provider toggle on or off for a specific domain based on clicking the navbar menu icon or a context menu therein? You could optionally allow a side route for requests additionally such as via cookie/localStorage which would allow them to exist throughout the lifecycle of the web3 provider extension, as mentioned earlier, but a user could simply be instructed to “click the fox icon if it’s gray” in the Metamask example.

By the way, part of me wonders if a web3 access request system opens the browser up to CPU usage measurement attacks which would negate the purpose of provider obfuscation anyway.

Hi @dustinvs. A true whitelist-based approach introduces its own UX issues by making all dapps inaccessible or unusable by default. Even if users got used to a flow of explicitly approving each new dapp they want to access, it seems odd to ask users to whitelist or approve a new dapp before they even see it. How does a user know they want to whitelist a dapp unless they can view it first? If instead you’re suggesting that users can still view a dapp but the provider would be disabled unless the dapp is explicitly whitelisted, I’m not sure how this UX is any better than EIP-1102 as written: dapps would still need to add fallback UX if they aren’t whitelisted and need to know if they’re in a dapp browser. This also requires users to know each browser-specific mechanism to enable a dapp (e.g. MetaMask may use an icon click, Status may use something different on mobile, etc.)

Allowing dapps to request provider access dynamically and only when needed using a standard DOM API guarantees Ethereum user privacy in a browser-agnostic manner. It’s also been suggested to use this same postMessage-based protocol for parameterized provider requests, such as requesting a specific account type on a specific network, something whitelisting can’t easily support.

The request / response protocol seems extensible, implementable, and safe, but the UX limitation of being unable to detect non-dapp browsers is a win for privacy but a potential loss for UX (at least based on flows that currently rely on global injection.)

It’s not functionally different to, or incompatible with, any of the approaches to requesting access that have been discussed (edit, that is, besides a parameterized one). Consider it a suggestion for implementing user-initiated requests outside of the scope of the dapp. You’re right that it’s not quite a replacement.

1 Like

We are currently using that approach for the Gnosis Safe Extension and in first user testing experienced that this is quite confusing for the user.

UX wise I find this very complicated, since for the user it is easier if they are being told were to click and what to do. If you have to open the browser extension and enable whitelisting it makes it quite a lot harder for the users imo.

What I had in mind was simply the dapp instructing the user to install the extension and then click the icon in the navbar to enable or similar. It’s possibly worse UX than an app-initiated request, especially one based on a mouse/keyboard/touch event, although I suspect that if it’s standardized and intuitive then it’s an issue of training the user to perform a single action for provider injection, as opposed to a different action for each site (if access cannot easily be requested dynamically by the app). If the available options for whatever reason can no longer include an app-initiated request, then something like that becomes a must. The UX issues are of course significantly different on mobile and desktop. Anyway, this may be out of band for the discussion.

The protocol defined in EIP-1102 was originally intended to both standardize the way dapp browsers expose a provider and to do so in a way that makes it impossible to uniquely identify Ethereum users from non-Ethereum users. Despite the technical feasibility of this approach and the “total privacy” it provides, it requires a monumental shift in dapp UX: user on-boarding is made more difficult because dapps can’t detect if a user has a dapp browser installed. We initially thought this level of privacy was clearly ideal, and only after the announcement of our rollout plan did the implied dapp UX receive full scrutiny.

Rethinking total privacy

Taking a step back, the original motivation behind EIP-1102 was to make provider exposure less obtrusive and safer. The proposal satisfies these requirements by requiring explicit user approval before exposing a provider.

But the proposal goes one step further by also removing any fingerprintable API surface from the dapp context. This means that dapps aren’t informed when a user rejects provider access or if they don’t have a dapp browser installed. The idea behind this was to prevent malicious websites from associating IP addresses with Ethereum use and running targeted attacks. But this type of total privacy becomes less important as more browsers with unique user agents add crypto features such as Brave, Opera, or Toshi. Since these browsers are already identifiable by user agent and because they all support Ethereum use either directly or indirectly, malicious sites can already fingerprint many users as potential Ethereum users regardless of EIP-1102 or an injected provider.

Considering these new UX findings and realizing that this type of fingerprint prevention is firmly stopgap in a future of maintstream, crypto-enabled browsers, it feels natural to revisit the goals and approach behind EIP-1102.

An updated proposal

After talking with various community members, weighing informative UX feedback, and realistically analyzing security risks around provider exposure, we drafted updates to EIP-1102 in which DOM environments expose a read-only provider until full provider access is approved by the user. By injecting a restricted provider on page load, dapps can still detect correct browser installation and initiate RPC requests to properly render their UI; by requiring user approval before fully-enabling the provider, users are still protected from unsolicited transactions and unwanted account exposure on untrusted websites.

EIP-1102: Proposed updates (feedback encouraged)

cc @danfinlay @andrey @serso @ryanio @dustinvs @rmeissner @JaceHensley @ricburton @dustinvs

3 Likes

Hello Paul, I am Everton Fraga from the ethereum Mist team.

Great job on the proposed changes. I am glad you considered the user-agent case, as Mist users are not protected against this kind of targeting.

But I thought about something that was sitting there even before you revisit this subject: if the majority of dapps happen to request {type: … id: 'metamask'} it would make no sense to non-Metamask DOM providers to take id into consideration and decide if it should respond to the request or not — they’d respond positively anyway.

So, to be future-proof and mitigate the risk of unnecessary fragmentation – or a potentially useless id parameter — if a dapp wants to make anything specific to metamask, instead of:

DO REQUEST METAMASK_PROVIDER
  SHOW FoxWithAHat
END

It could perform:

DO REQUEST ETHEREUM_PROVIDER
  IF PROVIDER === METAMASK
    SHOW FoxWithAHat
  END
END

Cheers!

Hi @everton, thanks for the feedback. The id property was originally intended to allow dapps to specify which wallet provider should service their request in scenarios where a user has multiple Ethereum extensions installed, but you’re right: this shouldn’t be codified into the proposal. If an extension or browser wishes to only respond to specific provider requests, they can establish their own proprietary API to do so (an id property being one option.)

I updated the new draft of the proposal accordingly. Thanks again for the input.

1 Like

After successful community iteration with additional browser teams and dapp developers alike, EIP-1102 has been formally updated to reflect the latest thinking on user-approved provider exposure:

Browsers expose a provider populated with no accounts by default. Before initiating any RPC request that requires an account, like eth_sendTransaction, dapps must request a full provider by calling a new provider method, provider#enable. This method triggers the user interface that allows the user to approve or deny full provider access for a given dapp. If the user approves full provider access, the provider is populated with accounts and thus fully-enabled; if the user denies full provider access, the provider is left unchanged.

This latest version of EIP-1102 avoids any previously-discussed UX issues and is now live for further review. The new protocol actively being investigated and implemented by privacy-conscious browsers including MetaMask, Mist, Status, and imToken.

Additional feedback is both welcomed and encouraged.

1 Like

Personally I preferred the event driven solution more, since it also gave an alternative to the issue when the user has multiple web3 provider enabled. But as this might not really belong in this EIP it might make sense to remove it. (But I do think “multiple installed providers” is something that needs to be thought about, especially when more standard browsers add default support, see Opera)

Another question for me would be, are there any information exposed in the read-only provider, about what provider it is? I know from some of our projects that they require meta-mask right now. So currently they check isMetaMask, but by exposing this method in the read-only provider you already give out quite some info again (or maybe not? :smiley: ). I am asking this because we will probably expose a similar method for our provider and it would be nice to be consistent.

EDIT:
Why not make it possible that the injected ethereum object has information about all available providers. The dapp could then request which providers are installed and let the user choose (or if one is preferred just select that one).
When a provider tries to inject the ethereum object it checks if it exists if not it creates it. Afterwards it adds itself as a possible provider.
The flow for the dapp would stay nearly the same as right now, but it would be possible to support different providers.