diff --git a/main.js b/main.js index 670c402..bf4b651 100644 --- a/main.js +++ b/main.js @@ -18,7 +18,9 @@ const { createAccountPicker } = require('./lib/account-rotation'); const ClouddropUploader = require('./lib/clouddrop-upload'); const { checkForUpdate, prepareUpdate, launchPreparedUpdate, abortUpdate, createUpdateAnnouncementState } = require('./lib/updater'); const backupCrypto = require('./lib/backup-crypto'); -const { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } = require('./lib/online-backup'); +const { downloadOnlineBackup } = require('./lib/online-backup'); +const { createOnlineBackupKeyring } = require('./lib/online-backup-keyring'); +const { createOnlineBackupManager } = require('./lib/online-backup-manager'); const { createPortableSettingsSnapshot, prepareImportedSettings } = require('./lib/settings-backup'); const { createSettingsImportGate } = require('./lib/settings-import-gate'); const FolderMonitor = require('./lib/folder-monitor'); @@ -119,6 +121,18 @@ let dropTargetWindow = null; let tray = null; const configStore = new ConfigStore(app); configStore.setPerfLog((m) => { try { logInfo(m); } catch {} }); +const onlineBackupKeyring = createOnlineBackupKeyring({ + filePath: path.join(app.getPath('userData'), 'online-backup-keys.json') +}); +const onlineBackupManager = createOnlineBackupManager({ + keyring: onlineBackupKeyring, + loadSettings: async () => { + await waitForConfigStoreWrites(); + return createPortableSettingsSnapshot(configStore.load()); + }, + appVersion: () => app.getVersion(), + copyText: value => clipboard.writeText(value) +}); let uploadManager = null; let lastSessionSummary = null; let sourceDeleteJournal = null; @@ -154,6 +168,17 @@ async function waitForConfigStoreWrites() { await configStore.drainWrites(); } +function requireCanonicalOnlineBackupId(id) { + if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{22}$/.test(id)) { + throw new Error('Online-Sicherungs-ID ist ungültig'); + } + const decoded = Buffer.from(id, 'base64url'); + if (decoded.length !== 16 || decoded.toString('base64url') !== id) { + throw new Error('Online-Sicherungs-ID ist ungültig'); + } + return id; +} + function clearCloseFlushTimer() { if (closeFlushTimer) clearTimeout(closeFlushTimer); closeFlushTimer = null; @@ -2781,13 +2806,21 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => { } }); -ipcMain.handle('online-backup:create', async () => { +ipcMain.handle('online-backup:list-managed', () => onlineBackupManager.listManaged()); + +ipcMain.handle('online-backup:create-managed', () => onlineBackupManager.createManaged()); + +ipcMain.handle('online-backup:copy-managed', (_event, id) => { try { - await waitForConfigStoreWrites(); - const snapshot = createPortableSettingsSnapshot(configStore.load()); - const created = createOnlineBackup(snapshot, app.getVersion()); - await uploadOnlineBackup(created.record); - return { ok: true, key: created.key }; + return onlineBackupManager.copyManaged(requireCanonicalOnlineBackupId(id)); + } catch (error) { + return { ok: false, error: error.message || String(error) }; + } +}); + +ipcMain.handle('online-backup:delete-managed', (_event, id) => { + try { + return onlineBackupManager.deleteManaged(requireCanonicalOnlineBackupId(id)); } catch (error) { return { ok: false, error: error.message || String(error) }; } diff --git a/preload.js b/preload.js index e2eaf64..8dff4f5 100644 --- a/preload.js +++ b/preload.js @@ -79,7 +79,11 @@ contextBridge.exposeInMainWorld('api', { // Backup exportBackup: (options) => ipcRenderer.invoke('export-backup', options), importBackup: (legacyPassword) => ipcRenderer.invoke('import-backup', legacyPassword), - createOnlineBackup: () => ipcRenderer.invoke('online-backup:create'), + listManagedOnlineBackups: () => ipcRenderer.invoke('online-backup:list-managed'), + createManagedOnlineBackup: () => ipcRenderer.invoke('online-backup:create-managed'), + copyManagedOnlineBackup: (id) => ipcRenderer.invoke('online-backup:copy-managed', id), + deleteManagedOnlineBackup: (id) => ipcRenderer.invoke('online-backup:delete-managed', id), + createOnlineBackup: () => ipcRenderer.invoke('online-backup:create-managed'), restoreOnlineBackup: (key) => ipcRenderer.invoke('online-backup:restore', key), // Folder Monitor diff --git a/tests/package-build-files.test.js b/tests/package-build-files.test.js index 1bd6c17..6a4be5a 100644 --- a/tests/package-build-files.test.js +++ b/tests/package-build-files.test.js @@ -1,15 +1,39 @@ const test = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('node:fs'); const Module = require('node:module'); const path = require('node:path'); const packageJson = require('../package.json'); +const projectRoot = path.join(__dirname, '..'); + test('packages every Electron preload referenced by the main process', () => { assert.ok(packageJson.build.files.includes('preload.js')); assert.ok(packageJson.build.files.includes('preload-drop-target.js')); + assert.ok(packageJson.build.files.includes('lib/**/*')); assert.equal(packageJson.build.win.signAndEditExecutable, false); }); +test('exposes managed online backup operations through narrow IPC boundaries', () => { + const preloadSource = fs.readFileSync(path.join(projectRoot, 'preload.js'), 'utf8'); + const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8'); + + assert.match(preloadSource, /listManagedOnlineBackups:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:list-managed'\)/u); + assert.match(preloadSource, /createManagedOnlineBackup:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed'\)/u); + assert.match(preloadSource, /copyManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:copy-managed',\s*id\)/u); + assert.match(preloadSource, /deleteManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:delete-managed',\s*id\)/u); + assert.match(preloadSource, /createOnlineBackup:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed'\)/u); + assert.doesNotMatch(preloadSource, /ipcRenderer\.invoke\('online-backup:create'\)/u); + assert.match(mainSource, /path\.join\(app\.getPath\('userData'\),\s*'online-backup-keys\.json'\)/u); + assert.match(mainSource, /Buffer\.from\(id,\s*'base64url'\)/u); + assert.match(mainSource, /decoded\.toString\('base64url'\)\s*!==\s*id/u); + assert.doesNotMatch(mainSource, /ipcMain\.handle\('online-backup:create'/u); + assert.match(mainSource, /ipcMain\.handle\('online-backup:list-managed',[\s\S]*?onlineBackupManager\.listManaged\(\)/u); + assert.match(mainSource, /ipcMain\.handle\('online-backup:create-managed',[\s\S]*?onlineBackupManager\.createManaged\(\)/u); + assert.match(mainSource, /ipcMain\.handle\('online-backup:copy-managed',[\s\S]*?onlineBackupManager\.copyManaged\(/u); + assert.match(mainSource, /ipcMain\.handle\('online-backup:delete-managed',[\s\S]*?onlineBackupManager\.deleteManaged\(/u); +}); + test('afterPack brands the executable metadata shown by Windows', async () => { let editCall = null; const originalLoad = Module._load; diff --git a/tests/ui-smoke.js b/tests/ui-smoke.js index ce17ec5..923d73b 100644 --- a/tests/ui-smoke.js +++ b/tests/ui-smoke.js @@ -1377,8 +1377,11 @@ setTimeout(async () => { const onlineBackupKeyContract = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput")?.maxLength + "|" + document.getElementById("onlineBackupKeyInput")?.getAttribute("pattern")'); check('Online backup input enforces the 75-character MHU key format', onlineBackupKeyContract === '75|MHU2-[A-Za-z0-9_-]{70}'); - const onlineBackupBridge = await wc.executeJavaScript('typeof window.api.createOnlineBackup + "|" + typeof window.api.restoreOnlineBackup'); - check('Online backup uses a narrow preload bridge', onlineBackupBridge === 'function|function'); + const onlineBackupBridge = await wc.executeJavaScript('["listManagedOnlineBackups", "createManagedOnlineBackup", "copyManagedOnlineBackup", "deleteManagedOnlineBackup"].map(name => typeof window.api[name]).join("|")'); + check('Online backup uses a narrow managed preload bridge', onlineBackupBridge === 'function|function|function|function'); + + const managedOnlineBackupCopyResult = await wc.executeJavaScript('window.api.copyManagedOnlineBackup("AAAAAAAAAAAAAAAAAAAAAA").then(result => JSON.stringify(result))'); + check('Managed online backup copy never returns a complete key', !managedOnlineBackupCopyResult.includes('MHU2-')); const invalidOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-short"; document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled + "|" + document.getElementById("onlineBackupStatus").textContent'); check('Invalid online backup keys stay blocked with visible guidance', invalidOnlineBackup === 'true|Der Schlüssel muss exakt 75 Zeichen lang sein.');