Add encrypted LAN sync between MrTerm devices: UDP discovery, X25519 pairing with short authentication code, mutually authenticated AES-256-GCM sessions, last-writer-wins merge with deletions, and per-item opt-in for SSH keys, passwords and VPN configurations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+32
-7
@@ -15,6 +15,7 @@ const { VpnManager } = require('./vpn');
|
||||
const { DockerManager } = require('./docker');
|
||||
const { FirewallManager } = require('./firewall');
|
||||
const { NetworkConfigManager } = require('./network');
|
||||
const { SyncService } = require('./sync');
|
||||
const applyLanguage = () => i18n.setLanguage(store.get().settings.language, app.getLocale());
|
||||
|
||||
let win;
|
||||
@@ -75,6 +76,17 @@ const docker = new DockerManager(ssh);
|
||||
const firewall = new FirewallManager(ssh);
|
||||
const network = new NetworkConfigManager(ssh);
|
||||
|
||||
// LAN-Synchronisation; Vergleichscode beim Koppeln bestätigt der Nutzer in der Oberfläche
|
||||
const pairReplies = new Map();
|
||||
const sync = new SyncService(store, send, (req) => new Promise((resolve) => {
|
||||
const reqId = crypto.randomUUID();
|
||||
pairReplies.set(reqId, resolve);
|
||||
send('sync:pairPrompt', { reqId, ...req });
|
||||
setTimeout(() => { if (pairReplies.delete(reqId)) resolve(false); }, 115000);
|
||||
}));
|
||||
ipcMain.on('sync:pairReply', (_e, reqId, ok) => { const r = pairReplies.get(reqId); pairReplies.delete(reqId); r?.(!!ok); });
|
||||
store.onChange = () => sync.schedule();
|
||||
|
||||
function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
width: 1360,
|
||||
@@ -134,7 +146,7 @@ function lockStatus() {
|
||||
}
|
||||
function requireUnlocked() { if (store.locked) throw new Error(i18n.t('MrTerm is locked.')); }
|
||||
const ensureLock = () => (store.lock = store.lock || { password: null, fido: [] });
|
||||
function afterUnlock() { applyLanguage(); scheduleUpdateCheck(); }
|
||||
function afterUnlock() { applyLanguage(); scheduleUpdateCheck(); sync.start().catch(() => {}); }
|
||||
|
||||
handle('lock:status', lockStatus);
|
||||
handle('lock:lock', () => { if (store.lockEnabled) store.locked = true; return lockStatus(); });
|
||||
@@ -190,7 +202,9 @@ handle('lock:removeFido', (id) => {
|
||||
});
|
||||
|
||||
// ---------- Vault ----------
|
||||
handle('vault:get', () => ({ ...store.get(), encrypted: store.encrypted, platform: process.platform }));
|
||||
// Kopplungsschlüssel und Löschvermerke bleiben im Hauptprozess
|
||||
const publicData = () => { const { sync: _s, tombstones: _t, ...rest } = store.get(); return rest; };
|
||||
handle('vault:get', () => ({ ...publicData(), encrypted: store.encrypted, platform: process.platform }));
|
||||
handle('vault:upsert', async (col, item) => {
|
||||
// Geänderte VPN-Konfiguration: alte Systemverbindung entfernen, beim nächsten Verbinden neu importieren
|
||||
if (col === 'vpns' && item.id) {
|
||||
@@ -202,7 +216,7 @@ handle('vault:upsert', async (col, item) => {
|
||||
handle('vault:remove', async (col, id) => {
|
||||
if (col === 'vpns') {
|
||||
await vpn.forget(store.get().vpns.find((v) => v.id === id));
|
||||
store.get().hosts.forEach((h) => { if (h.vpnId === id) h.vpnId = null; });
|
||||
store.get().hosts.forEach((h) => { if (h.vpnId === id) { h.vpnId = null; h.updatedAt = Date.now(); } });
|
||||
}
|
||||
return store.remove(col, id);
|
||||
});
|
||||
@@ -245,6 +259,16 @@ handle('network:readFile', (id, path) => network.readFile(id, path));
|
||||
handle('network:writeFile', (id, path, content, verify) => network.writeFile(id, path, content, verify));
|
||||
handle('network:close', (id) => network.close(id));
|
||||
|
||||
// ---------- Synchronisation ----------
|
||||
handle('sync:status', () => sync.status());
|
||||
handle('sync:enable', (on, name) => sync.setEnabled(on, name));
|
||||
handle('sync:pairable', (on) => { sync.setPairable(on); return sync.status(); });
|
||||
handle('sync:pair', (id) => sync.pair(id));
|
||||
handle('sync:unpair', (id) => sync.unpair(id));
|
||||
handle('sync:now', () => sync.syncAll());
|
||||
handle('sync:share', (sel) => sync.setShare(sel));
|
||||
handle('sync:probe', (address) => sync.probe(address));
|
||||
|
||||
// ---------- VPN ----------
|
||||
handle('vpn:status', () => vpn.status());
|
||||
handle('vpn:up', (id) => vpn.up(id));
|
||||
@@ -254,18 +278,18 @@ handle('vault:settings', (s) => {
|
||||
if ('language' in s) applyLanguage();
|
||||
if ('rdpEmbed' in s) fs.writeFileSync(launchFile, JSON.stringify({ ...readLaunch(), x11: s.rdpEmbed !== false }));
|
||||
});
|
||||
handle('vault:forgetHost', (id) => { delete store.get().knownHosts[id]; store.save(); });
|
||||
handle('vault:forgetHost', (id) => store.forgetKnownHost(id));
|
||||
handle('vault:export', async () => {
|
||||
const r = await dialog.showSaveDialog(win, { defaultPath: 'mrterm-backup.json', filters: [{ name: 'JSON', extensions: ['json'] }] });
|
||||
if (r.canceled) return false;
|
||||
fs.writeFileSync(r.filePath, JSON.stringify(store.get(), null, 2), { mode: 0o600 });
|
||||
fs.writeFileSync(r.filePath, JSON.stringify(publicData(), null, 2), { mode: 0o600 });
|
||||
return r.filePath;
|
||||
});
|
||||
handle('vault:import', async () => {
|
||||
const r = await dialog.showOpenDialog(win, { filters: [{ name: 'JSON', extensions: ['json'] }], properties: ['openFile'] });
|
||||
if (r.canceled) return false;
|
||||
const data = JSON.parse(fs.readFileSync(r.filePaths[0], 'utf8'));
|
||||
for (const col of ['groups', 'hosts', 'keys', 'snippets', 'forwards'])
|
||||
for (const col of ['groups', 'hosts', 'keys', 'snippets', 'forwards', 'vpns'])
|
||||
for (const item of data[col] || []) store.upsert(col, item);
|
||||
return true;
|
||||
});
|
||||
@@ -525,6 +549,7 @@ app.whenReady().then(() => {
|
||||
store.load();
|
||||
applyLanguage();
|
||||
createWindow();
|
||||
sync.start().catch(() => {});
|
||||
scheduleUpdateCheck();
|
||||
});
|
||||
|
||||
@@ -535,7 +560,7 @@ function scheduleUpdateCheck() {
|
||||
updateScheduled = true;
|
||||
setTimeout(() => updater.check().then((r) => { if (r.available) send('update:available', r); }).catch(() => {}), 6000);
|
||||
}
|
||||
app.on('window-all-closed', async () => { ssh.closeAll(); await vpn.downOnQuit(); app.quit(); });
|
||||
app.on('window-all-closed', async () => { ssh.closeAll(); sync.stop(); await vpn.downOnQuit(); app.quit(); });
|
||||
|
||||
// Smoke-Test: MRTERM_SMOKE=<pfad.png> startet, loggt Renderer-Meldungen, speichert einen Screenshot und beendet.
|
||||
if (process.env.MRTERM_SMOKE) {
|
||||
|
||||
Reference in New Issue
Block a user