nips/146.md

80 lines
8.2 KiB
Markdown
Raw Normal View History

2024-10-25 11:10:37 -04:00
NIP-146
=======
Iframe-based Nostr Connect (INC)
--------------------------------
`draft` `optional`
This NIP defines a way for web apps to communicate with web signers embedded as iframes using browser APIs. It is applicable to self-custodial signers that store keys in the browser's local storage. Basically, client app does nip-46 RPC with an iframe using [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
Due to privacy-related restrictions, local storage of iframes is [partitioned](https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning) from top-frame's storage - iframe of signer.com can't see the storage of top-level signer.com and can't access user's private key. Solution to this challenge is the biggest part of this NIP.
Advantage: minimal latency, no relay involved, no reliance on unreliable webpush, fully cross-platform. User private key is as safe in iframe's local storage as in top-level storage due to browsers' cross-origin restrictions.
Disadvantage: potentially more frequent "confirmations", higher surface for web-based hacks.
## Terms
- `client`: a web app trying to access user's keys
- `signer`: non-custodial signer storing keys in it's local storage in the browser
- `top-level signer`: signer opened in a separate tab/popup
- `iframe signer`: signer opened as an iframe inside the client
- `worker iframe`: iframe signer that handles nip46 calls from the client
- `starter iframe`: iframe signer that launches the connection flow to get user confirmation and import keys from top-level signer to iframe signer
- `rebinder iframe`: iframe signer that launches re-connection flow to check that client has access to keys at the top-level signer and to re-import the keys to iframe signer
## Overview
When nip46 connection is established, client embeds invisible `worker iframe` and exchanges nip46 messages with it using `postMessage`.
To overcome partitioned storage, when client initiates nip46 connection and receives `auth_url`, it shows the `starter iframe` with "Continue" button. After user clicks, starter iframe launches auth_url popup to access top-level signer. When user confirms, top-level signer sends the user private key back to starter iframe using postMessage. User private key can then be used by `worker iframe`.
To overcome ephemeral partitioned storage ([Brave](https://brave.com/privacy-updates/7-ephemeral-storage/), Webkit), when worker iframe receives a request but has no target keys, it pauses the call and sends an error code. Client shows visible `rebinder iframe` with "Continue" button. When user clicks, rebinder iframe launches top-level signer in a popup, which checks if client pubkey has access to the user pubkey and if so sends user private key back to rebinder iframe, without user having to confirm anything.
## Worker iframe
Signer may signal support for this NIP by adding `nip46.iframe_url` field in their nostr.json file. If client detects a signer that supports iframes, after the connection has already been established, the client MAY create an invisible `worker` iframe with `src` set to `nip46.iframe_url`.
When worker iframe has started and is ready to process requests it must notify the client by calling `window.parent.postMessage("workerReady", "*")`. In turn, the client must subscribe to worker's messages with `window.addEventListener("message", handler)` before creating an iframe, and must wait for the message event with `data` set to `workerReady` and `origin` set to origin of `nip46.iframe_url` before sending requests.
When `workerReady` is received, the client MAY start sending nip-46 requests to the worker by calling `workerIframe.postMessage(signedNip46RequestEvent, originOf(nip46.iframe_url))` - this will send the request event JS object to the worker iframe and will ensure it is only delivered if iframe still has proper origin (signer).
Worker iframe processes the request and sends back the reply event by using `requestEvent.source.postMessage(signedNip46ResponseEvent, requestEvent.origin)`, this will deliver the reply to the client.
## Starter iframe
Due to local storage partitioning, an iframe must import the needed key from the top-level storage of the signer. To do that, iframe must obtain a reference to a top-level signer and exchange messages. This fits well with the need to open top-level signer popup with `auth_url` of the nip-46 connection flow. However, this time the popup opener must be the signer iframe, and it must follow a [user gesture](https://developer.mozilla.org/en-US/docs/Web/API/UserActivation) in iframe.
To achieve that, a `starter iframe` MAY be embedded by the client when `auth_url` is received while the client is connecting. Starter iframe MUST be served when `nip46.iframe_url` has `?auth_url=<auth_url>` parameter, and MUST display a button suggesting the users to `Continue`. Recommended button dimentions SHOULD be up to `180px` width and `80px` height.
When user clicks on `Continue` button in starter iframe, it MUST open the popup with `auth_url` and MUST accept the user's private key if user confirms in the popup. Details are implementation-defined, see recommendations below.
When starter iframe has finished importing the user private key from the popup, it MUST notify the app by calling `window.parent.postMessage("starterDone", "*")`. When client receives the `starterDone` from iframe_url's origin it MAY destroy the starter iframe and MAY create the worker iframe to start processing requests.
## Rebinder iframe
When worker iframe receives a nip46 request targeting user pubkey that it doesn't have (local storage deleted) it MAY pause the call and then MUST reply with a string `errorNoKey:<requestEvent.id>` to notify the client.
If client receives `errorNoKey:<requestEvent.id>` reply matching one of pending requests, it MAY display a `rebinder iframe`. Rebinder iframe MUST be served when `nip46.iframe_url` has `&rebind=<local_pubkey>&pubkey=<remote_user_pubkey>` parameters, and MUST display a button similar to starter iframe suggesting the users to `Continue`.
When user clicks on `Continue` button in rebinder iframe, it MUST open a top-level signer popup. The popup MUST check that provided `local_pubkey` has permissions to access `remote_user_pubkey` and then MUST pass the user private key to rebinder iframe. Details are implementation-defined, see appendix for recommendations.
After rebinder iframe has finished importing the user private key, it MUST notify the client by calling `window.parent.postMessage("rebinderDone", "*")`. When client receives the `rebinerDone` from iframe_url's origin it MAY destroy the rebinder iframe.
When worker iframe that has paused the request notices that user private key was imported by rebinder into the local store, it MUST restart the paused call and deliver the reply to the client.
## Recommendations
- Popups SHOULD be opened using `window.open(url, "<random_target>", "<options>")`, `<random_target>` is required (instead of `_blank`) to make sure popup has access to `opener`, `noopener` SHOULD NOT be provided for the same reason.
- Iframes SHOULD NOT launch a popup until their service worker has started, it seems like Safari pauses iframe's SW initialization if the tab loses focus
- Starter/rebinder iframes MAY be sandboxed, but then MUST at least have `allow-scripts,allow-same-origin,allow-popups-to-escape-sandbox`
- Starter iframe SHOULD be provided access to `referrer` to let the signer use it as client name/url.
- Starter iframe SHOULD check that `auth_url` has valid origin.
- Starter/rebinder MAY use `MessageChannel` to create a port that will be transferred to top-level signer and to it's service-worker so that it could then export the user private key to the iframe signer.
- Client MAY save the `iframe_url` to local storage and reuse it until logout.
- Client MAY send normal nip-46 requests over relay in paralles to iframe requests, in which case it SHOULD deduplicate the replies across both channels.
- Signer SHOULD use random auxiliary sub-domains to serve `iframe_url` - Chrome desktop allows users to delete signer's storage on client's tab, which (mistakenly?) removes top-level signer storage too and users' keys might be lost.
- Top-level signer MAY export `local client pubkey` to iframe along with user private key to scope the iframe signer to this particular connection.