A page served by the gateway that signs in with the Pithos key file (Ed25519 in Web Crypto) or an Export JSON S3 key, then browses, views, downloads, uploads, renames and deletes, encrypting and decrypting in the page with the same formats as the desktop. Checked against a local gateway and real s3d: files the desktop encrypted open byte-identical in the browser, and browser uploads open in the desktop client; the server only ever holds encrypted names. Remember on this device keeps the key sealed with a passphrase in IndexedDB.
131 lines
6.9 KiB
Markdown
131 lines
6.9 KiB
Markdown
# Pithos in the browser (plan)
|
||
|
||
Status: 2026-10-05. Phases 1-3 built and tested locally (crypto port, gateway / + /_app/ route, sign-in with a key file, browse, view, download, upload, new folder, rename, delete, remember). Not deployed: needs DNS for pithos.silentmode.st. Decided: `https://pithos.silentmode.st`, sign-in only, "Remember on this device" allowed.
|
||
|
||
## Goal
|
||
|
||
Open your drives from any browser, on any device, with nothing installed:
|
||
browse, upload, download, rename, delete and share files on **Drives on
|
||
Silent Mode** (the hosted gateway at `https://s3.silentmode.st`).
|
||
|
||
Out of scope: a drive on your own computer (`http://127.0.0.1:8000`). s3d
|
||
sends no CORS headers, so a web page cannot talk to it; for that the website
|
||
hands over to the local Pithos ("Open my Pithos", port 47621).
|
||
|
||
## Where it lives: its own origin
|
||
|
||
**`https://pithos.silentmode.st`**: a vhost of its own that serves the app
|
||
files and forwards `/hosted/v1/*`, `/api/*` and the S3 paths to the hosted
|
||
gateway (127.0.0.1:8720), so the page and the API share one origin.
|
||
|
||
- Not `app.pithos.sia` either: outside Theseus BNS names are paths of
|
||
navigate.st (and BNS folds subdomains into the parent name's folder), so
|
||
the origin would still be navigate.st. Per-name origins on navigate.st
|
||
(`<name>.navigate.st`, wildcard certificate) would fix that for every BNS
|
||
site; a separate project.
|
||
- Not on `navigate.st/bns/pithos.sia/app`: every BNS site served through
|
||
navigate.st shares the origin `navigate.st`, so any other `.sia`/`.bch` site
|
||
could open the app's window (same origin) and read the keys out of it, or
|
||
read anything it stored.
|
||
- Same origin as the API, so **no CORS** has to be opened on the gateway at
|
||
all, and the browser keeps the keys away from every other site.
|
||
- `pithos.sia/app` keeps its meaning: in Theseus it is the full app (the
|
||
add-on answers it); in any other browser the website's app-address hint
|
||
gets a third button, "Open in the browser app", linking to
|
||
`https://pithos.silentmode.st`.
|
||
- Strict CSP (`default-src 'self'`, `connect-src 'self'`, no inline script,
|
||
`frame-ancestors 'none'`), no third-party scripts, files served by the
|
||
gateway from `Pithos/web/` (versioned with Pithos).
|
||
|
||
## Signing in
|
||
|
||
Two kinds of key, both loaded from a file (drag and drop or picker) or pasted:
|
||
|
||
1. **Your Pithos key** (owner, full access, encrypted drives readable).
|
||
- Without Theseus: the existing export, `pithos-hosted-key.json`
|
||
(`{v:1, secret}`), Accounts › On Silent Mode › Export key. From the secret:
|
||
identity seed = HKDF(secret, "", "pithos/hosted-identity/v1", 32),
|
||
encryption master = HKDF(secret, "", "pithos/hosted-encryption/v1", 32).
|
||
- With Theseus there is no file: the keys come from the vault. New export
|
||
**"Key for the browser app"** (vault unlocked + Theseus approval prompt):
|
||
`{v:2, identitySeed, encryptionKey}`, the two 32-byte values derived from
|
||
the vault paths `pithos/hosted-identity/v1` and
|
||
`pithos/hosted-encryption/v1`. NEVER change those paths. The file is as
|
||
powerful as the vault for these drives; the dialog says so.
|
||
- Flow: `GET /hosted/v1/challenge` → Ed25519 signature in WebCrypto
|
||
(PKCS8 wrapper `302e020100300506032b657004220420 || seed`) →
|
||
`POST /hosted/v1/session` → Bearer token → `GET /api/keys` → the account's
|
||
S3 key → S3 calls signed in the page.
|
||
2. **An S3 key file** (the Export JSON format `{endpoint, accessKey,
|
||
secretKey, bucket}`), including folder keys (grant keys `PG…`) from Share….
|
||
No encryption key: plain files and shared folders (stored readable) work,
|
||
encrypted names show as "encrypted file". The endpoint must be the
|
||
gateway.
|
||
|
||
Keys live **in memory only** by default; closing the tab signs out.
|
||
"Remember on this device" (allowed): the key file is wrapped with a
|
||
passphrase (PBKDF2-SHA256 600k + AES-GCM, as Save credentials… does) in
|
||
IndexedDB on the pithos.silentmode.st origin; the next visit asks for the
|
||
passphrase only. Sign out wipes it and calls
|
||
`DELETE` on the session.
|
||
|
||
## Crypto in the page
|
||
|
||
Port `core/crypt.js` and the signing part of `core/s3.js` to browser ESM
|
||
in `Pithos/web/lib/`, sharing test vectors with the Node versions.
|
||
|
||
- HKDF-SHA256, AES-256-GCM (bodies), AES-CTR, HMAC-SHA256 (SigV4): WebCrypto.
|
||
- AES-SIV (RFC 5297) for names is not in WebCrypto:
|
||
- CMAC/S2V from AES-CBC with a zero IV (WebCrypto appends a PKCS#7
|
||
block: take the block before it); one-block AES-ECB the same way.
|
||
- SIV-CTR with AES-CTR `counter = V'` (bits cleared as crypt.js does),
|
||
`length: 128`.
|
||
- No library: @noble/ciphers' `siv` is AES-GCM-SIV (RFC 8452), not RFC 5297.
|
||
- Tests: RFC 5297 vectors, plus Node-encrypts / browser-decrypts and back for
|
||
names (with the bucket + parent-path AD) and bodies (chunk sizes, ranges,
|
||
empty file), run under Node's WebCrypto in `npm test`.
|
||
|
||
## What the app does (v1)
|
||
|
||
- Drives list, folder browsing (merged encrypted + plain listing, as
|
||
`core/hosted.js` does), icons/list views.
|
||
- Upload: drag and drop; encrypted chunk by chunk (1 MiB) in the page;
|
||
multipart above ~64 MiB so big files never sit in memory whole.
|
||
- Download: decrypt as a stream (File System Access API where available,
|
||
else a Blob; very large files on browsers without it get a warning).
|
||
- Rename, move, delete, new folder (renames re-encrypt, as the desktop does).
|
||
- Share: public folder / link / folder key through `/hosted/v1/rules`.
|
||
- Viewer for pictures and text, audio/video player using range reads.
|
||
- Free space and account state (`/hosted/v1/me`, `/api/space`).
|
||
|
||
Later: the docked music player, offline cache, PWA install.
|
||
|
||
## Server changes
|
||
|
||
- DNS `pithos.silentmode.st` → the VPS; certificate; an Angie vhost that
|
||
serves `Pithos/web/` at `/` (strict CSP, versioned file names, long cache)
|
||
and proxies `/hosted/v1/`, `/api/` and S3 requests to 127.0.0.1:8720.
|
||
- The gateway must accept that Host for SigV4: the page signs requests for
|
||
host `pithos.silentmode.st`; owner keys go raw to s3d, which checks the
|
||
signature over the Host it receives, so the vhost passes `Host` through
|
||
unchanged and `publicUrl`-based checks (challenge message) need the second
|
||
name (the challenge text names the URL the client signed in at).
|
||
- No CORS: same origin. Path rules unchanged (owner keys raw-proxied to s3d;
|
||
grant keys checked by `rules.js`). Sign-in rate limits apply.
|
||
|
||
## Phases
|
||
|
||
1. Crypto + SigV4 port with cross-tests (largest risk, done first).
|
||
2. Gateway `/app` route + sign-in with a key file + read-only browsing and
|
||
downloads.
|
||
3. Uploads, folders, rename/delete.
|
||
4. Sharing, viewer and player.
|
||
5. "Key for the browser app" export in Theseus; website button; docs.
|
||
|
||
## Open questions
|
||
|
||
- Decided 2026-10-05: `pithos.silentmode.st`; sign-in only (accounts are
|
||
made in Pithos); "Remember on this device" allowed (passphrase-wrapped).
|
||
- Still open: does s3d accept SigV4 for a Host other than the one it was set
|
||
up with? (It checks the signature over whatever Host arrives, so it should;
|
||
verify in phase 2.)
|