summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/app.js125
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/i18n.js21
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js15
3 files changed, 150 insertions, 11 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js
index 0aa74b4..94bf09e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js
@@ -113,6 +113,57 @@ function _saveSessionKeys() {
if (_sessionKeys) sessionStorage.setItem('meshbay_sk', JSON.stringify(_sessionKeys));
} catch {}
}
+/**
+ * Rough passphrase strength, in bits, and what it is up against.
+ *
+ * This number carries more weight here than in most applications. The encrypted
+ * keypair bundle is protected by PBKDF2-SHA512 (600k) and sits on every node
+ * whose group you join, so the people who host your groups can attack it offline
+ * (finding C4). PBKDF2 is memory-light, which is exactly what GPUs are good at.
+ *
+ * The estimate is deliberately conservative — character classes and length, with
+ * a penalty for repetition and for the handful of patterns everyone tries. It is
+ * a guide, not a guarantee, and it says so in the UI.
+ */
+function passwordBits(pw) {
+ if (!pw) return 0;
+ let pool = 0;
+ if (/[a-z]/.test(pw)) pool += 26;
+ if (/[A-Z]/.test(pw)) pool += 26;
+ if (/[0-9]/.test(pw)) pool += 10;
+ if (/[^A-Za-z0-9]/.test(pw)) pool += 32;
+ let bits = pw.length * Math.log2(pool || 1);
+
+ const unique = new Set(pw).size;
+ if (unique < pw.length / 2) bits *= 0.6; // "aaaaaaaa", "abcabcabc"
+ if (/^[0-9]+$/.test(pw)) bits *= 0.5; // dates, PINs
+ if (/(password|motdepasse|azerty|qwerty|123456|meshbay)/i.test(pw)) bits *= 0.3;
+ return Math.round(bits);
+}
+
+const PASSWORD_MIN_BITS = 60; // refuse below this
+const PASSWORD_MIN_LEN = 12;
+
+// Whether this account backs its encrypted keys up to the nodes it joins.
+//
+// On: any browser recovers the same identity with the password — the ordinary
+// multi-device expectation. Off: the keys exist only where they were generated,
+// nothing is left on anyone's disk, and losing this browser's storage loses the
+// account's content for good. That is a real choice, so it is the user's.
+const KEY_BACKUP_PREFIX = 'mb_key_backup_';
+
+function keyBackupEnabled(username) {
+ try {
+ return localStorage.getItem(KEY_BACKUP_PREFIX + username) !== '0';
+ } catch { return true; }
+}
+
+function setKeyBackupEnabled(username, on) {
+ try {
+ localStorage.setItem(KEY_BACKUP_PREFIX + username, on ? '1' : '0');
+ } catch {}
+}
+
function _restoreSessionKeys() {
try {
if (!_sessionKeys) {
@@ -432,7 +483,14 @@ function RegisterPage() {
const onSubmit = async (e) => {
e.preventDefault();
if (password !== confirm) { setError(t('register.err_mismatch')); return; }
- if (password.length < 8) { setError(t('register.err_min_len')); return; }
+ if (password.length < PASSWORD_MIN_LEN) {
+ setError(t('register.err_min_len', { n: PASSWORD_MIN_LEN })); return;
+ }
+ // The floor can only live here: with the password split (T1) the hub never
+ // sees the password, so it cannot enforce anything about it.
+ if (passwordBits(password) < PASSWORD_MIN_BITS) {
+ setError(t('register.err_too_weak')); return;
+ }
setError('');
setLoading(true);
try {
@@ -480,6 +538,18 @@ function RegisterPage() {
<input type="password" placeholder="${t('register.password')}" value=${password}
onInput=${e => setPassword(e.target.value)}
autocomplete="new-password" required minlength="8" />
+ ${password && html`
+ <div style="margin:-4px 0 10px">
+ <div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden">
+ <div style=${`height:100%;width:${Math.min(100, passwordBits(password) / 100 * 100)}%;
+ background:${passwordBits(password) < PASSWORD_MIN_BITS ? 'var(--error)'
+ : passwordBits(password) < 80 ? 'var(--yellow, #f59e0b)' : 'var(--success)'}`}></div>
+ </div>
+ <p style="font-size:0.8em;color:var(--text-dim);margin-top:4px">
+ ${t('register.strength', { bits: passwordBits(password) })}
+ </p>
+ </div>
+ `}
<input type="password" placeholder="${t('register.confirm')}" value=${confirm}
onInput=${e => setConfirm(e.target.value)}
autocomplete="new-password" required />
@@ -909,14 +979,26 @@ function GroupPage({ groupId, group, token, username, userId, onRefreshAuth }) {
}
}
- // Push keypair bundle to node (new registration, localStorage → node)
- if (_pendingBundlePush && transport.connected) {
- try {
- await transport.storeKeypairBundle(_pendingBundlePush);
- try { localStorage.removeItem(`meshbay_kp_${username}`); } catch {}
- _pendingBundlePush = null;
- } catch (e) {
- console.warn('[MeshBay] Bundle push to node deferred:', e.message);
+ // Back the encrypted keys up to the node, or withdraw them — whichever
+ // this account asked for. The local copy is only dropped once the node
+ // holds one, so turning backup off never strands anybody.
+ if (transport.connected) {
+ if (keyBackupEnabled(username)) {
+ if (_pendingBundlePush) {
+ try {
+ await transport.storeKeypairBundle(_pendingBundlePush);
+ try { localStorage.removeItem(`meshbay_kp_${username}`); } catch {}
+ _pendingBundlePush = null;
+ } catch (e) {
+ console.warn('[MeshBay] Bundle push to node deferred:', e.message);
+ }
+ }
+ } else {
+ try {
+ await transport.deleteKeypairBundle();
+ } catch (e) {
+ console.warn('[MeshBay] Could not withdraw key backup:', e.message);
+ }
}
}
@@ -2124,6 +2206,15 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
const [nodeKeyLoading, setNodeKeyLoading] = useState(false);
const [pinCount, setPinCount] = useState(
() => (window.MeshBayTransport?.pinnedNodeCount?.() ?? 0));
+ const [backup, setBackup] = useState(() => keyBackupEnabled(user.username));
+
+ // Takes effect on the next connection to each node: enabling uploads the
+ // encrypted bundle, disabling withdraws whatever that node already holds.
+ const toggleBackup = useCallback(() => {
+ const next = !backup;
+ setKeyBackupEnabled(user.username, next);
+ setBackup(next);
+ }, [backup, user.username]);
// 11.5.8: node identity pins are refused strictly on change, so users need a
// deliberate way to accept a legitimate rotation (operator reinstalled a node).
@@ -2222,6 +2313,22 @@ function SettingsPage({ user, theme, onThemeChange, groups }) {
</div>
<div class="settings-section">
+ <h3 class="settings-heading">${t('settings.key_backup')}</h3>
+ <p class="settings-hint">${t('settings.key_backup_hint')}</p>
+ <div class="settings-row">
+ <span class="settings-label">
+ ${backup ? t('settings.key_backup_on') : t('settings.key_backup_off')}
+ </span>
+ <button class="btn-secondary" onClick=${toggleBackup}>
+ ${backup ? t('settings.key_backup_disable') : t('settings.key_backup_enable')}
+ </button>
+ </div>
+ ${!backup && html`
+ <p class="error-msg" style="margin-top:8px">${t('settings.key_backup_warning')}</p>
+ `}
+ </div>
+
+ <div class="settings-section">
<h3 class="settings-heading">${t('settings.node_pins')}</h3>
<p class="settings-hint">${t('settings.node_pins_hint')}</p>
<div class="settings-row">
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
index c279fd1..702da0b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/i18n.js
@@ -38,7 +38,7 @@ const en = {
'register.title': 'Register',
'register.username': 'Username',
'register.email': 'Email',
- 'register.password': 'Password (min 8 chars)',
+ 'register.password': 'Passphrase (min 12 chars)',
'register.confirm': 'Confirm password',
'register.submit': 'Register',
'register.loading': 'Creating account...',
@@ -48,7 +48,11 @@ const en = {
'register.success_msg': 'You can now log in with your credentials.',
'register.go_login': 'Go to login',
'register.err_mismatch': 'Passwords do not match',
- 'register.err_min_len': 'Password must be at least 8 characters',
+ 'register.err_min_len': 'Use at least {n} characters',
+ 'register.err_too_weak': 'Too easy to guess. Your passphrase is what protects '
+ + 'your keys where they are stored — a few unrelated words work well.',
+ 'register.strength': 'Strength: about {bits} bits. This protects the copy of '
+ + 'your keys kept on the nodes you join, so it is worth getting right.',
// Home
'home.welcome': 'Welcome to MeshBay',
@@ -120,6 +124,19 @@ const en = {
'settings.coming_soon': 'Coming soon.',
'settings.profile': 'Profile',
'settings.username': 'Username',
+ 'settings.key_backup': 'Use this account on other devices',
+ 'settings.key_backup_hint': 'Your keys can be kept — encrypted with your '
+ + 'passphrase — on the nodes whose groups you join, so another browser can '
+ + 'recover them. Turn this off and your keys stay only where they were '
+ + 'created: nothing of yours sits on anyone else\'s disk, and only that '
+ + 'browser can open your groups.',
+ 'settings.key_backup_on': 'Enabled — other browsers can recover your keys',
+ 'settings.key_backup_off': 'Disabled — keys stay in this browser only',
+ 'settings.key_backup_enable': 'Enable',
+ 'settings.key_backup_disable': 'Disable',
+ 'settings.key_backup_warning': 'With this off, clearing this browser\'s data '
+ + 'loses access to everything encrypted for you. There is no recovery — that '
+ + 'is the point. Copies already on nodes are withdrawn at the next connection.',
'settings.node_pins': 'Node identities',
'settings.node_pins_hint': "Each node's identity key is remembered the first time you connect. If it changes, the connection is refused — that is expected only when an operator reinstalls a node. Verify with them before clearing.",
'settings.node_pins_count': '{n} pinned',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 5ead80e..b1a03ff 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -574,6 +574,21 @@ class MeshBayTransport {
return gekRaw;
}
+ /**
+ * Withdraw our key backup from this node.
+ *
+ * The counterpart of storeKeypairBundle: turning the setting off has to remove
+ * what is already stored, not merely stop adding to it — otherwise the blob
+ * stays on every node the account has ever joined (C4).
+ */
+ async deleteKeypairBundle() {
+ const msg = await this._sendAndWait({
+ type: 'keypair_bundle_delete', v: '0.1',
+ });
+ if (msg.type === 'error') throw new Error(msg.detail);
+ return msg;
+ }
+
async storeKeypairBundle(bundleEnc) {
const msg = await this._sendAndWait({
type: 'keypair_bundle_store',