Skip to content

hazae41/dstorage

Repository files navigation

DStorage

Proof-of-concept of a public and secure origin-agnostic storage for your origin-less apps.

Why

This allows your website to use the storage (KeyValue and WebAuthn) of another origin.

This is particularily useful when your website doesn't have a single origin.

For example, if you use the same app from multiple origins (e.g. browser extensions, mobile webviews).

Or if you want maximum decentralization by allowing multiple origins to operate on the same storage (e.g. IPFS websites).

For example, allowing both https://myapp.ipfs.io and https://myapp.example.com to have the same storage.

Or you just want some basic versioning like https://1.myapp.org and https://2.myapp.org.

Communication

A few modes of cross-origin communication are available depending on your constraints.

Service-worker to service-worker communication (page-bootstrapped)

You just need to open a server page to bootstrap the communication from your service-worker to the server service-worker.

You must keep one server page open in order to keep the server service-worker running.

This communication continues to work even a few seconds after all pages are closed (~30 seconds on Chromium).

When the communication is closed, just reopen a new bootstrap page.

Page to service-worker communication (page-bootstrapped)

This is the same as above but this time the communication is closed once you close the client page.

Service-worker to service-worker communication (iframe-bootstrapped)

Only available on Chrome with document.requestStorageAccess({ caches: true }).

You use an iframe to bootstrap the communication and maintain the server service-worker running.

No page is opened except when asking for user interaction.

Page to service-worker communication (iframe-bootstrapped)

This is the same as above but this time the communication is closed once you close the client page.

APIs

All communication is done via JSON-RPC 2.0.

Example

Given the function

function example(value: boolean): void

You can call it with

port.postMessage([{ jsonrpc: "2.0", id: 123, method: "example", params: [true] }])

And get the result with

const [{ id, result, error }] = event.data

Cache

This is a key-value storage using Cache API.

The access requires user-interaction

function kv_ask(scope: string, capacity: number): void

This will ask user-interaction for access to scope and grow it to capacity bytes

function kv_set(scope: string, request: RequestLike, response: ResponseLike): void

This will set key to value in scope

function kv_get(scope: string, request: RequestLike): unknown

This will return value from key in scope

(RequestLike and ResponseLike are just JSON-compatible versions of Request and Response)

WebAuthn

This is a remote version of WebAuthnStorage.

This will open a page requiring user confirmation.

function webauthn_storage_create(name: string, data: Uint8Array): Uint8Array
function webauthn_storage_get(handle: Uint8Array): Uint8Array

Limitations

The main limitation is storage availability.

On some browsers the storage is not persistent unless the user adds the storage website to his favorites.

Also, as explained below, an attacker can try to fill the storage with random stuff until it's full.

This can be mitigated by only allowing a limited storage from user-approved origins using a confirmation page.

This can make the UX worse than just using a local storage, but can prevent phishing because a new origin will show a warning.

Security

Since all operation are cross-origin through JSON-like messages, there is no much security risk as the browser sandboxes everything.

Attacker from a third-party website

All storages do not have discoverability, so there is no risk of an attacker seeing or tampering with the data.

He would need to know the storage keys of the data.

Bruteforce is not viable if you use strong random-like keys like UUIDs or hashes.

Even if he can guess the keys, assuming your data is strongly encrypted, he would only be able to delete that data.

Another possible attack is filling the storage with random stuff to cause the storage to be full.

This can be mitigated by the storage website; only allowing a limited storage from user-approved origins.

Attacker from the storage website

If the storage website is compromised (supply-chain, DNS, BGP attack), there is not much it can do.

Assuming your data is strongly encrypted, it can't do anything beside deleting that data or refusing to serve it.

Attacker from your website

If your own website is compromised then the storage is probably available to the attacker, just like any local storage.

You can somewhat mitigate this by encrypting your data using an user-provided password or some WebAuthn authentication.

Protocols

Explicit Transfer Protocol (ETP)

All messages sent via postMessage are in the format [message: unknown, transferreds: Transferable[]]

e.g.

const bytes = new Uint8Array([1,2,3,4,5])

window.postMessage([{
  method: "example",
  params: [bytes]
}, [bytes]], [bytes])

This allows explicit passing of transferable objects and thus zero-copy messaging and proxying

e.g.

/**
 * Proxy and transfer from port to port2
 */
port.onmessage = (e) => {
  const [m, t] = e.data

  /**
   * t are transferred again
   */
  port2.postMessage([m, t], t)
}

D-JSON-RPC (UDP-like multicast-like communication)

This is used via postMessage by pages (and iframes) and service-workers before a MessagePort is shared

Format

This is like JSON-RPC but there is no request-response

There are only JSON messages using the following format

{
  method: string,
  params: unknown
}

ping

Perform a ping to ensure the target is available for a connect or connect2

Only used for page targets as for service-worker targets we can use serviceWorker.ready heuristic

{
  method: "ping"
}

The target is available if a pong is received

  • Why use ping when there is already hello?

A ping can be sent from/to a middlebox whereas hello is always sent end-to-end

  • Why don't use a connected or connected2 reply?

Because all communication here is multicast-like so a connected may trigger others

pong

Reply to a ping

{
  method: "pong"
}

connect

Establish an end-to-end communication to the target

{
  method: "connect"
}

The message also contains a MessagePort to use as a bidirectional port

connect2

Establish an end-to-end communication to the target's service-worker

{
  method: "connect2"
}

The message also contains a MessagePort to use as a bidirectional port

connect3

Sent by a page to its service-worker after a connect2

{
  method: "connect3",
  params: [trueOrigin]
}

The message also contains a MessagePort to use as a bidirectional port

JSON-RPC (TCP-like unicast-like communication)

Once a end-to-end communication is established via MessagePort

hello

Perform a fast bidirectional active-passive handshake to ensure the target is available

{
  method: "hello"
}

The connection is ready when both sides received either a hello request or a hello response

About

Origin-agnostic storage for your dapp

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published