Proof-of-concept of a public and secure origin-agnostic storage for your origin-less apps.
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.
A few modes of cross-origin communication are available depending on your constraints.
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.
This is the same as above but this time the communication is closed once you close the client page.
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.
This is the same as above but this time the communication is closed once you close the client page.
All communication is done via JSON-RPC 2.0.
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
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
)
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
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.
Since all operation are cross-origin through JSON-like messages, there is no much security risk as the browser sandboxes everything.
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.
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.
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.
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)
}
This is used via postMessage
by pages (and iframes) and service-workers before a MessagePort
is shared
This is like JSON-RPC but there is no request-response
There are only JSON messages using the following format
{
method: string,
params: unknown
}
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 alreadyhello
?
A ping
can be sent from/to a middlebox whereas hello
is always sent end-to-end
- Why don't use a
connected
orconnected2
reply?
Because all communication here is multicast-like so a connected
may trigger others
Reply to a ping
{
method: "pong"
}
Establish an end-to-end communication to the target
{
method: "connect"
}
The message also contains a MessagePort
to use as a bidirectional port
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
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
Once a end-to-end communication is established via MessagePort
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