aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client/src/main.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client/src/main.js')
-rw-r--r--packages/meshbay-client/src/main.js74
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) => {