aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-client
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-client')
-rw-r--r--packages/meshbay-client/scripts/sync-ui.js2
-rw-r--r--packages/meshbay-client/src/keyring.js34
-rw-r--r--packages/meshbay-client/src/main.js38
3 files changed, 57 insertions, 17 deletions
diff --git a/packages/meshbay-client/scripts/sync-ui.js b/packages/meshbay-client/scripts/sync-ui.js
index d0b6eee..4ebdf35 100644
--- a/packages/meshbay-client/scripts/sync-ui.js
+++ b/packages/meshbay-client/scripts/sync-ui.js
@@ -23,7 +23,7 @@ const DEST = path.resolve(__dirname, '..', 'ui');
const OURS = ['index.html'];
// sw.js has to sit at the root of the scope it serves, which it already does.
-const SKIP = new Set(['webrtc-test.html']);
+const SKIP = new Set();
function copyTree(from, to) {
fs.mkdirSync(to, { recursive: true });
diff --git a/packages/meshbay-client/src/keyring.js b/packages/meshbay-client/src/keyring.js
index 4fb6eac..ec37fd4 100644
--- a/packages/meshbay-client/src/keyring.js
+++ b/packages/meshbay-client/src/keyring.js
@@ -24,6 +24,8 @@ const { transcriptFor } = require('./transcripts.js');
// keyderive.js: the same numbers, or no bundle opens across the two.
const ARGON2 = { memory: 131072, passes: 3, parallelism: 1, tagLength: 32 };
const MAGIC = Buffer.from('MBK3');
+// TRANSITIONAL — the format before MBK3, read once to be replaced (keyderive.js).
+const LEGACY_MAGIC = Buffer.from('MBK2');
const X25519_SPKI = Buffer.from('302a300506032b656e032100', 'hex');
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
@@ -63,6 +65,17 @@ function seal(identity, key, userId, nodePk, pepperVersion) {
return b64(Buffer.concat([MAGIC, Buffer.from([pepperVersion & 0xff]), nonce, ct]));
}
+/** TRANSITIONAL — MBK2: "MBK2" ‖ nonce ‖ AES-GCM under the Argon2 key, no AAD. */
+function openLegacy(bundleB64, key) {
+ const raw = unb64(bundleB64);
+ const nonce = raw.subarray(4, 16);
+ const body = raw.subarray(16, raw.length - 16);
+ const d = crypto.createDecipheriv('aes-256-gcm', key, nonce);
+ d.setAuthTag(raw.subarray(raw.length - 16));
+ const plain = JSON.parse(Buffer.concat([d.update(body), d.final()]).toString());
+ return { ed: plain.skEd, x: plain.skX };
+}
+
function open(bundleB64, key, userId, nodePk) {
const raw = unb64(bundleB64);
if (!raw.subarray(0, 4).equals(MAGIC)) {
@@ -142,7 +155,11 @@ function createKeyring({ load, save, argon2 }) {
const v = pepperVersion || 1;
if (p) { pending.set(userId, { m, v }); return true; }
const s = state();
- s.masters[userId] = { m: b64(m), v };
+ // `legacy` (TRANSITIONAL): the Argon2 key itself, which MBK2 bundles
+ // were sealed under — kept beside `M`, in the same OS-protected store
+ // and for as long, so a node still holding one has it opened and
+ // replaced on the next connection. Remove once no MBK2 bundle is left.
+ s.masters[userId] = { m: b64(m), v, legacy: b64(a) };
save(s);
return true;
},
@@ -150,7 +167,10 @@ function createKeyring({ load, save, argon2 }) {
const p = pending.get(userId);
if (!p) return false;
const s = state();
- s.masters[userId] = { m: b64(p.m), v: p.v };
+ // The legacy key stays the old passphrase's: MBK2 bundles were sealed
+ // under that one, never under the new.
+ const legacy = (s.masters[userId] || {}).legacy;
+ s.masters[userId] = { m: b64(p.m), v: p.v, ...(legacy ? { legacy } : {}) };
save(s);
pending.delete(userId);
return true;
@@ -177,6 +197,16 @@ function createKeyring({ load, save, argon2 }) {
* was entered (a reset on a machine that had never held this identity).
*/
openBundle(userId, nodePk, { bundleEnc, recoveryEnc, recoveryMnemonic, username }) {
+ if (unb64(bundleEnc).subarray(0, 4).equals(LEGACY_MAGIC)) {
+ // TRANSITIONAL. Kept unsealed (`sealedWith: null`), so the next
+ // settle replaces the node's copy with MBK3, or withdraws it when the
+ // account has no browser access.
+ const legacy = (state().masters[userId] || {}).legacy;
+ if (!legacy) throw new Error('no_legacy_key');
+ const id = openLegacy(bundleEnc, unb64(legacy));
+ keep(userId, nodePk, { ...id, sealedWith: null });
+ return publicOf(id);
+ }
const { m } = master(userId);
let id;
try {
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 309d5f6..c1ff540 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -1160,8 +1160,24 @@ function registerBridge() {
return { path: chosen, name: path.basename(chosen) };
});
+ // The Mark-of-the-Web, as a browser leaves on every download: the file came
+ // from somebody else's machine, and Windows decides what that means —
+ // SmartScreen for a program, Protected View for a document. This application
+ // writes its files itself, so nothing else marks them. NTFS only; elsewhere
+ // there is no such stream, and nothing is lost by not having one.
+ function markFromInternet(file) {
+ if (process.platform !== 'win32') return;
+ try {
+ fs.writeFileSync(`${file}:Zone.Identifier`, '[ZoneTransfer]\r\nZoneId=3\r\n');
+ } catch { /* FAT, exFAT, a network share: no alternate data streams */ }
+ }
+
handle('save:begin', async (_e, suggestedName, opts) => {
- const wanted = path.basename(String(suggestedName || 'download'));
+ // Bidirectional controls replaced here as well as in the page
+ // (portable-name.js): "invoice\u202efdp.exe" would be saved, and listed by
+ // the file manager, as "invoiceexe.pdf".
+ const wanted = path.basename(String(suggestedName || 'download'))
+ .replace(/[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, '_');
const chosen = chosenDownloadDir();
let target = null;
@@ -1231,6 +1247,7 @@ function registerBridge() {
console.error('[MeshBay] could not finalise download:', err.message);
return false;
}
+ markFromInternet(sink.path);
completedPaths.set(String(id), sink.path);
return true;
});
@@ -2184,9 +2201,11 @@ function registerBridge() {
attachGroup: async (a) => {
const body = { name: aText(a.name, 'the group name'),
shared_dir: aText(a.path, 'the folder', 4096),
- writable: a.writable !== false };
- await confirmOrRefuse('native.attach_confirm',
- { name: body.name, path: body.shared_dir });
+ writable: a.writable !== false,
+ // The person's choice on the creation form; the node never
+ // takes it from the hub.
+ join_policy: a.joinPolicy === 'open' ? 'open' : 'invite' };
+ await confirmFolder(body.shared_dir);
return ['POST', '/api/groups/attach', body];
},
detachGroup: (a) => ['POST', '/api/groups/detach', { name: aText(a.name, 'the group name') }],
@@ -2203,16 +2222,7 @@ function registerBridge() {
ejectRoot: (a) => ['PUT', `${root(a)}/eject`],
plugRoot: (a) => ['PUT', `${root(a)}/plug`],
removeRoot: (a) => ['DELETE', root(a)],
- // Creating a missing key replaces nothing -- the node keeps an existing one
- // -- so only a rotation is asked about.
- initGek: async (a) => {
- const target = group(a);
- if (a.rotate) {
- await confirmOrRefuse('node.gek_rotate_confirm');
- return ['POST', `${target}/gek?rotate=true`];
- }
- return ['POST', `${target}/gek`];
- },
+ initGek: (a) => ['POST', `${group(a)}/gek${a.rotate ? '?rotate=true' : ''}`],
pairOperator: () => ['POST', '/api/operator/pair'],
roster: (a) => ['GET', a.groupId
? `/api/roster?group_id=${anId(a.groupId, 'the group')}` : '/api/roster'],