diff options
Diffstat (limited to 'packages/meshbay-client/src')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 86 | ||||
| -rw-r--r-- | packages/meshbay-client/src/preload.js | 4 |
2 files changed, 84 insertions, 6 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 3fc52ff..867ce27 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -30,16 +30,53 @@ const { pathToFileURL } = require('node:url'); const UI_DIR = path.join(__dirname, '..', 'ui'); const SCHEME = 'app'; +// The policy, sent as a header on every response. +// +// Not a <meta> tag: `frame-ancestors` is ignored there — Chromium says so in +// the console — and a policy with a directive that silently does nothing is +// worse than one without it. A header is authoritative for every directive, +// and this handler is the only thing that serves the interface, so there is one +// source rather than two that can drift. +// +// `'wasm-unsafe-eval'` is required and must stay: the bundle key is Argon2id in +// WebAssembly, and a policy without it does not degrade anything — it locks +// every user out of their keys. +// +// The hub is reachable under connect-src, for its API and its signaling socket. +// It is deliberately absent from script-src: nothing it returns is executed, +// which is the whole reason this application exists (T3). +const CSP = [ + "default-src 'none'", + "script-src 'self' 'wasm-unsafe-eval'", + "style-src 'self' 'unsafe-inline'", + "img-src 'self' data: blob:", + "media-src 'self' blob:", + "font-src 'self'", + "connect-src 'self' https: wss:", + "worker-src 'self'", + "frame-ancestors 'none'", + "base-uri 'none'", + "form-action 'none'", +].join('; '); + // ── The scheme the interface is served from ───────────────────────────────── // // Not file://. Service workers, ES modules and IndexedDB all misbehave there, // and the streamed-download path needs a *controlled* page — a worker that is // merely active is not enough, which this codebase has already learned once. // -// `secure` is what makes it a secure context, without which the worker refuses -// to register and downloads break with no error at all. `standard` gives it a -// real origin, so IndexedDB survives an update instead of being keyed to -// something that moves. +// `secure` is what makes it a secure context, and without it the whole of +// `crypto.subtle` is undefined — measured on Electron 42 / Chromium 148, where +// a page on a non-secure scheme failed every algorithm including AES-GCM. +// `standard` gives it a real origin, so IndexedDB survives an update instead of +// being keyed to something that moves. +// +// What it does NOT buy: service workers. Chromium refuses to register one on a +// custom scheme whatever its privileges ("The URL protocol of the current +// origin is not supported"). So `sw.js` never runs here, and the streamed +// download it exists for is replaced by the native save dialog below — which is +// the better path anyway. It stays in the package because the same files serve +// the browser, where it is one of only three ways to write a large file. protocol.registerSchemesAsPrivileged([{ scheme: SCHEME, privileges: { @@ -70,7 +107,11 @@ function registerUiProtocol() { try { const body = await fsp.readFile(target); return new Response(body, { - headers: { 'Content-Type': contentType(target) }, + headers: { + 'Content-Type': contentType(target), + 'Content-Security-Policy': CSP, + 'X-Content-Type-Options': 'nosniff', + }, }); } catch { return new Response('Not found', { status: 404 }); @@ -252,6 +293,39 @@ function registerBridge() { return url; }); + // Every call to the hub leaves from here, not from the renderer. + // + // Not a preference. The page's origin is `app://meshbay`, and a browser fetch + // from it is refused by CORS — the hub has no CORS middleware at all, which is + // a posture worth keeping: its API is reachable from no web origin whatever. + // Widening it for `app://meshbay` would be worse than it looks, because that + // origin is not a credential — any Electron application on any machine can + // claim the same scheme and host. + // + // So the renderer asks and this process goes, exactly as it does for saving a + // file. Node's fetch has no origin and no CORS, the hub stays closed to the + // web, and there is one place where network egress happens. + ipcMain.handle('hub:fetch', async (_e, url, init) => { + const target = new URL(String(url)); + const base = config.hubBase ? new URL(config.hubBase) : null; + // The renderer may only reach the hub it is signed in to. A path it + // controls must not become a request to somewhere else. + if (!base || target.origin !== base.origin) { + throw new Error('Refused: not this hub'); + } + const response = await fetch(target, { + method: (init && init.method) || 'GET', + headers: (init && init.headers) || {}, + body: (init && init.body) || undefined, + }); + return { + status: response.status, + ok: response.ok, + headers: Object.fromEntries(response.headers), + body: await response.text(), + }; + }); + ipcMain.handle('secrets:backend', () => secretsBackend()); ipcMain.handle('secrets:get', (_e, name) => readSecrets()[String(name)] ?? null); ipcMain.handle('secrets:set', (_e, name, value) => { @@ -328,4 +402,4 @@ if (!app.requestSingleInstanceLock()) { }); } -module.exports = { contentType, secretsBackend }; +module.exports = { contentType, secretsBackend, CSP }; diff --git a/packages/meshbay-client/src/preload.js b/packages/meshbay-client/src/preload.js index 9c273e2..e3e240f 100644 --- a/packages/meshbay-client/src/preload.js +++ b/packages/meshbay-client/src/preload.js @@ -42,6 +42,10 @@ contextBridge.exposeInMainWorld('meshbay', { nativeSave: true, }, + // Ask the main process to call the hub. The renderer has an `app://` origin, + // which CORS refuses and which is not a credential anyway. + fetch: (url, init) => ipcRenderer.invoke('hub:fetch', url, init), + secrets: { get: (name) => ipcRenderer.invoke('secrets:get', name), set: (name, value) => ipcRenderer.invoke('secrets:set', name, value), |