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
+10 -4
View File
@@ -1,11 +1,13 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { isIP } = require('node:net');
const secretStore = require('./secret-store');
const { parseOnlineBackupKey } = require('./online-backup');
const STORED_LEGACY_ENTRY_KEYS = ['createdAt', 'encryptedKey', 'id'];
const STORED_EXPIRING_ENTRY_KEYS = ['createdAt', 'encryptedKey', 'expiresAt', 'id'];
const STORED_IP_ENTRY_KEYS = ['createdAt', 'encryptedKey', 'expiresAt', 'id', 'sourceIp'];
const STORED_V1_DOCUMENT_KEYS = ['keys', 'version'];
const STORED_GENERATED_DOCUMENT_KEYS = ['generation', 'keys', 'version'];
const KEYRING_ERROR_CODES = Object.freeze({
@@ -181,13 +183,15 @@ function createOnlineBackupKeyring({
function validateEntry(entry) {
const hasLegacyShape = hasExactKeys(entry, STORED_LEGACY_ENTRY_KEYS);
const hasExpiringShape = hasExactKeys(entry, STORED_EXPIRING_ENTRY_KEYS);
const hasIpShape = hasExactKeys(entry, STORED_IP_ENTRY_KEYS);
const hasExpiringShape = hasExactKeys(entry, STORED_EXPIRING_ENTRY_KEYS) || hasIpShape;
if (
(!hasLegacyShape && !hasExpiringShape)
|| !isCanonicalId(entry.id)
|| typeof entry.encryptedKey !== 'string'
|| !isEncrypted(entry.encryptedKey)
|| typeof entry.createdAt !== 'string'
|| (hasIpShape && (typeof entry.sourceIp !== 'string' || !isIP(entry.sourceIp)))
) {
return { issue: KEYRING_ERROR_CODES.structure, id: typeof entry?.id === 'string' ? entry.id : null };
}
@@ -228,6 +232,7 @@ function createOnlineBackupKeyring({
encryptedKey: entry.encryptedKey,
createdAt,
expiresAt,
...(hasIpShape ? { sourceIp: entry.sourceIp } : {}),
key
}
};
@@ -413,7 +418,7 @@ function createOnlineBackupKeyring({
const contents = JSON.stringify({
version: 2,
generation,
keys: entries.map(({ id, encryptedKey, createdAt, expiresAt }) => ({ id, encryptedKey, createdAt, expiresAt: expiresAt ?? null }))
keys: entries.map(({ id, encryptedKey, createdAt, expiresAt, sourceIp }) => ({ id, encryptedKey, createdAt, expiresAt: expiresAt ?? null, ...(sourceIp ? { sourceIp } : {}) }))
});
const payload = canonicalPayload(parseDocument(contents));
const stagingPath = temporaryPath('staging');
@@ -455,11 +460,12 @@ function createOnlineBackupKeyring({
}
const entries = activeEntries
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
.map(({ id, key, createdAt, expiresAt }) => Object.freeze({
.map(({ id, key, createdAt, expiresAt, sourceIp }) => Object.freeze({
id,
displayKey: `${key.slice(0, 9)}${key.slice(-4)}`,
createdAt,
expiresAt
expiresAt,
...(sourceIp ? { sourceIp } : {})
}));
return Object.freeze({
entries: Object.freeze(entries),
+7 -4
View File
@@ -29,7 +29,8 @@ function sanitizeEntry(entry) {
id: entry.id,
displayKey: entry.displayKey,
createdAt: entry.createdAt,
expiresAt: entry.expiresAt ?? null
expiresAt: entry.expiresAt ?? null,
...(entry.sourceIp ? { sourceIp: entry.sourceIp } : {})
};
}
@@ -38,7 +39,8 @@ function sanitizeCreatedEntry(entry, key) {
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 } : {})
};
}
@@ -80,8 +82,9 @@ function createOnlineBackupManager({
const settings = await loadSettings();
const recoveryPublicKey = await loadRecoveryPublicKey();
const created = createBackup(settings, appVersion(), createdAt, retention, recoveryPublicKey);
const prepared = keyring.prepare(created.key, createdAt, created.expiresAt ?? null);
await uploadBackup(created.record);
let prepared = keyring.prepare(created.key, createdAt, created.expiresAt ?? null);
const metadata = await uploadBackup(created.record);
if (metadata?.sourceIp) prepared = Object.freeze({ ...prepared, sourceIp: metadata.sourceIp });
try {
await keyring.commit(prepared);
} catch (error) {
+7 -1
View File
@@ -1,5 +1,6 @@
const crypto = require('node:crypto');
const zlib = require('node:zlib');
const { isIP } = require('node:net');
const ONLINE_BACKUP_API_URL = 'https://uploader.24-music.de/backup-api';
const KEY_PREFIX = 'MHU2-';
@@ -238,12 +239,17 @@ async function downloadRecoveryPublicKey(baseUrl = ONLINE_BACKUP_API_URL, option
}
async function uploadOnlineBackup(record, baseUrl = ONLINE_BACKUP_API_URL, options) {
const { response } = await requestText(endpoint(baseUrl, '/v1/backups'), {
const { response, body } = await requestText(endpoint(baseUrl, '/v1/backups'), {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(record)
}, options);
if (response.status !== 201) throw new Error('Online-Sicherung konnte nicht gespeichert werden');
try {
const metadata = JSON.parse(body);
if (typeof metadata.sourceIp === 'string' && isIP(metadata.sourceIp)) return { sourceIp: metadata.sourceIp };
} catch {}
return {};
}
async function downloadOnlineBackup(key, baseUrl = ONLINE_BACKUP_API_URL, options) {