Track online backup source IP and add detailed recovery output
CI / verify (push) Waiting to run

This commit is contained in:
Sucukdeluxe
2026-09-22 05:02:21 +02:00
parent 560966fa2a
commit 43b77401c6
15 changed files with 147 additions and 26 deletions
+8
View File
@@ -6,6 +6,14 @@ const espree = require('espree');
const { normalizeLanguage, translateText } = require('../renderer/i18n');
test('online backup metadata displays source IP without exposing full keys', () => {
assert.equal(translateText('IP unbekannt', 'en'), 'IP unknown');
assert.equal(translateText('IP unknown', 'de'), 'IP unbekannt');
const source = fs.readFileSync(path.join(__dirname, '../renderer/app.js'), 'utf8');
assert.ok(source.includes('`IP: ${entry.sourceIp}`'));
assert.ok(source.includes('key.textContent = entry.displayKey;'));
});
test('online backup selection defaults to unlimited in both languages', () => {
assert.equal(translateText('Unbegrenzt (Standard)', 'en'), 'Unlimited (default)');
assert.equal(translateText('Unlimited (default)', 'de'), 'Unbegrenzt (Standard)');
+14
View File
@@ -70,6 +70,20 @@ function writeKeyring(filePath, keys, generation = null) {
}
describe('encrypted online backup keyring', () => {
it('persists valid source IPs across reloads alongside legacy entries', async () => {
const { keyring, filePath } = fixture();
const first = { ...keyring.prepare(validKey(), timestamp), sourceIp: '2001:db8::42' };
await keyring.commit(first);
await keyring.commit(keyring.prepare(validKey(), timestamp));
const listed = await keyring.list();
assert.equal(listed.issues.length, 0);
assert.equal(listed.entries.find(entry => entry.id === first.id).sourceIp, '2001:db8::42');
assert.equal(JSON.parse(fs.readFileSync(filePath, 'utf8')).keys.find(entry => entry.id === first.id).sourceIp, '2001:db8::42');
await assert.rejects(keyring.commit({ ...keyring.prepare(validKey(), timestamp), sourceIp: 'invalid' }));
const removal = await keyring.prepareRemove(first.id);
await keyring.commitRemove(removal);
assert.equal((await keyring.list()).entries.length, 1);
});
it('persists the spec keys schema without plaintext and returns frozen sanitized entries', async () => {
const { filePath, keyring } = fixture();
const key = validKey();
+12 -1
View File
@@ -60,7 +60,8 @@ function createFixture(overrides = {}) {
id: entry.id,
displayKey: `${key.slice(0, 9)}${key.slice(-4)}`,
createdAt: entry.createdAt,
expiresAt: entry.expiresAt ?? null
expiresAt: entry.expiresAt ?? null,
...(entry.sourceIp ? { sourceIp: entry.sourceIp } : {})
});
},
getKey: async (entryId) => {
@@ -101,6 +102,7 @@ function createFixture(overrides = {}) {
events.push('upload');
assert.equal(uploadedRecord, record);
if (overrides.uploadError) throw overrides.uploadError;
return overrides.uploadMetadata;
},
deleteBackup: async (value) => {
events.push(`delete:${value}`);
@@ -176,6 +178,15 @@ function deleteVerifier(value) {
}
describe('transactional online backup manager', () => {
it('keeps server-provided source IP in the committed and listed metadata', async () => {
const fixture = createFixture({ initialKey: null, uploadMetadata: { sourceIp: '2001:db8::1' } });
const created = await fixture.manager.createManaged();
assert.equal(created.ok, true);
assert.equal(created.entry.sourceIp, '2001:db8::1');
const listed = await fixture.manager.listManaged();
assert.equal(listed.entries[0].sourceIp, '2001:db8::1');
assert.ok(!JSON.stringify(listed).includes(key));
});
it('creates in prepare, upload, commit order and returns only the sanitized entry', async () => {
const fixture = createFixture({ initialKey: null });
+15 -2
View File
@@ -36,13 +36,14 @@ test('server persists recovery without plaintext secrets, supports normal import
const publicKey = await downloadRecoveryPublicKey(url);
assert.equal(publicKey, pair.publicKey);
const backup = createOnlineBackup({ language: 'de', password: 'test-secret' }, 'test', undefined, '1d', publicKey);
await uploadOnlineBackup(backup.record, url);
assert.deepEqual(await uploadOnlineBackup(backup.record, url), { sourceIp: '127.0.0.1' });
const recordPath = join(rootDir, `${backup.record.id}.json`);
const raw = await readFile(recordPath, 'utf8');
assert.ok(!raw.includes(backup.key));
assert.ok(!raw.includes('test-secret'));
const record = JSON.parse(raw);
assert.equal(record.version, 3);
assert.equal(record.version, 4);
assert.equal(record.sourceIp, '127.0.0.1');
assert.equal(recoverBackupKey(backup.record.id, record, pair.privateKey), backup.key);
assert.equal((await downloadOnlineBackup(backup.key, url)).settings.language, 'de');
const privateFile = join(rootDir, 'private.pem');
@@ -53,6 +54,18 @@ test('server persists recovery without plaintext secrets, supports normal import
assert.equal(result.status, 0, result.stderr);
assert.equal((await readFile(output, 'utf8')).trim(), backup.key);
assert.ok(!result.stdout.includes(backup.key));
const detailedOutput = join(rootDir, 'details.txt');
assert.equal(spawnSync(process.execPath, [cli, 'recover', recordPath, privateFile, detailedOutput, '--details']).status, 0);
const details = await readFile(detailedOutput, 'utf8');
assert.match(details, /^\d{2}\.\d{2}\.\d{4} - \d{2}:\d{2} \| IP: 127\.0\.0\.1 \| MHU2-/);
assert.ok(details.trim().endsWith(backup.key));
const legacyRecord = { ...record, version: 3 };
delete legacyRecord.sourceIp;
await writeFile(recordPath, JSON.stringify(legacyRecord));
assert.equal((await downloadOnlineBackup(backup.key, url)).settings.language, 'de');
const legacyDetails = join(rootDir, 'legacy-details.txt');
assert.equal(spawnSync(process.execPath, [cli, 'recover', recordPath, privateFile, legacyDetails, '--details']).status, 0);
assert.ok((await readFile(legacyDetails, 'utf8')).includes(' | IP: unbekannt | '));
assert.equal(spawnSync(process.execPath, [cli, 'recover', recordPath, privateFile, output]).status, 1);
const other = createOnlineBackup({}, 'test', undefined, 'forever', publicKey);
await uploadOnlineBackup(other.record, url);