diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-30 16:58:31 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-30 16:58:31 +0200 |
| commit | 6d167392f6f8ede37e2794a68a3738f8ba03131d (patch) | |
| tree | 9caacef15dd034c6425f4bb623e0cd50a28ec52a /packages/meshbay-client/src/main.js | |
| parent | 8926f163dad9d32dc06c3a142658a4e11d9c12c1 (diff) | |
| download | meshbay-6d167392f6f8ede37e2794a68a3738f8ba03131d.tar.gz | |
feat(client): the desktop application keeps M and every node identity in its main process
keyring.js derives, opens, mints, seals, signs and agrees there; the page gets
public keys and a handle. Argon2 comes from the page's own WebAssembly build
(Electron's crypto has none). Without OS key storage the page keeps its keys as
a browser does. A node's bundle is settled after connecting, re-sealed when the
key changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-client/src/main.js')
| -rw-r--r-- | packages/meshbay-client/src/main.js | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index 5e1120b..9a08e96 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -31,6 +31,8 @@ const os = require('node:os'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const vm = require('node:vm'); +const { createKeyring } = require('./keyring.js'); +const { wasmArgon2 } = require('./argon2-wasm.js'); // Linux window managers/desktop shells (GNOME's dash included) group and // icon-match a running window by its WM_CLASS, resolved against an installed @@ -482,6 +484,8 @@ function secretsBackend() { // operators, and nothing wraps a group key for it. const DEVICE_KEY = 'device_auth_ed25519'; +// The account's bundle master key and node identities (keyring.js). +const KEYRING_SLOT = 'keyring_v1'; function deviceKey() { const stored = readSecrets()[DEVICE_KEY]; @@ -979,6 +983,76 @@ function registerBridge() { // the OS protects the store. handle('secrets:backend', () => secretsBackend()); + // ── The account's keys (keyring.js) ───────────────────────────────────── + // + // `M` and every node identity stay in this process; the page is answered + // with public keys, signatures and agreements. Only where the OS can protect + // what is stored: an identity kept here and lost at the next start would + // leave a node pinning a key nobody holds, so without key storage the page + // keeps its keys the way a browser does. + const keysAvailable = () => secretsBackend() !== 'unavailable'; + const keyring = createKeyring({ + argon2: wasmArgon2(path.join(UI_DIR, 'vendor')), + load: () => { try { return JSON.parse(readSecrets()[KEYRING_SLOT] || '{}'); } catch { return {}; } }, + save: (state) => { + const all = readSecrets(); + all[KEYRING_SLOT] = JSON.stringify(state); + writeSecrets(all); + }, + }); + const uid = (v) => { + const s = String(v || ''); + if (!/^[0-9a-f-]{36}$/i.test(s)) throw new Error('Refused: not an account id'); + return s; + }; + const npk = (v) => { + const s = String(v || ''); + if (!/^[A-Za-z0-9+/=]{1,100}$/.test(s)) throw new Error("Refused: not a node's key"); + return s; + }; + const needKeys = () => { if (!keysAvailable()) throw new Error('No OS key storage'); }; + + handle('keys:available', () => keysAvailable()); + handle('keys:derive-session', async (_e, o) => { + needKeys(); + const a = o || {}; + return keyring.deriveSession({ + password: String(a.password || ''), username: String(a.username || ''), + userId: uid(a.userId), pepperB64: String(a.pepperB64 || ''), + pepperVersion: Number(a.pepperVersion) || 1, pending: Boolean(a.pending), + }); + }); + handle('keys:commit-pending', (_e, u) => keyring.commitPending(uid(u))); + handle('keys:drop-pending', (_e, u) => keyring.dropPending(uid(u))); + handle('keys:has-session', (_e, u) => keysAvailable() && keyring.hasSession(uid(u))); + handle('keys:forget-session', (_e, u) => keyring.forgetSession(uid(u))); + handle('keys:identity', (_e, u, n) => keyring.identity(uid(u), npk(n))); + handle('keys:open-bundle', (_e, u, n, o) => keyring.openBundle(uid(u), npk(n), { + bundleEnc: String((o && o.bundleEnc) || ''), + })); + handle('keys:mint', (_e, u, n) => { needKeys(); return keyring.mint(uid(u), npk(n)); }); + handle('keys:seal-bundle', (_e, u, n, o) => + keyring.sealBundle(uid(u), npk(n), { pending: Boolean(o && o.pending) })); + handle('keys:seal-recovery', (_e, u, n, mnemonic, username) => + keyring.sealRecovery(uid(u), npk(n), String(mnemonic || ''), String(username || ''))); + handle('keys:mark-sealed', (_e, u, n, fp) => keyring.markSealed(uid(u), npk(n), String(fp || ''))); + handle('keys:fingerprint', (_e, u) => keyring.currentFingerprint(uid(u))); + handle('keys:sign', (_e, u, n, bytes) => keyring.sign(uid(u), npk(n), String(bytes || ''))); + handle('keys:shared', (_e, u, n, peer) => keyring.shared(uid(u), npk(n), String(peer || ''))); + handle('keys:playlist-key', (_e, u) => keyring.playlistKey(uid(u))); + handle('keys:browser-access', (_e, u) => keyring.browserAccess(uid(u))); + // Turning it on puts this account's identities on every node, sealed for a + // browser to open with the passphrase: the person decides that here, in a + // dialog the page cannot answer. Turning it off only narrows. + handle('keys:set-browser-access', async (_e, u, on) => { + const id = uid(u); + if (on && !keyring.browserAccess(id)) await confirmOrRefuse('native.browser_access_confirm'); + return keyring.setBrowserAccess(id, Boolean(on)); + }); + // An account created on this device starts without browser access. Only + // ever narrows, so the page may say it. + handle('keys:created-here', (_e, u) => keyring.setBrowserAccess(uid(u), false)); + // Which catalogue native confirmations are worded from. A language code and // nothing else: an unknown one leaves the current language in place. handle('ui:locale', (_e, code) => { |