This commit is contained in:
@@ -54,6 +54,7 @@ function createOnlineBackupManager({
|
||||
createBackup = createOnlineBackup,
|
||||
uploadBackup = uploadOnlineBackup,
|
||||
deleteBackup = deleteOnlineBackup,
|
||||
loadRecoveryPublicKey = async () => null,
|
||||
copyText
|
||||
}) {
|
||||
let mutation = Promise.resolve();
|
||||
@@ -76,7 +77,8 @@ function createOnlineBackupManager({
|
||||
async function createTransaction(retention) {
|
||||
const createdAt = new Date().toISOString();
|
||||
const settings = await loadSettings();
|
||||
const created = createBackup(settings, appVersion(), createdAt, retention);
|
||||
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);
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
function recoveryPublicKey(pem) {
|
||||
if (typeof pem !== 'string' || pem.length > 8192 || !pem.startsWith('-----BEGIN PUBLIC KEY-----')) throw new Error('Ungültiger öffentlicher Wiederherstellungsschlüssel');
|
||||
const key = crypto.createPublicKey(pem);
|
||||
if (key.asymmetricKeyType !== 'rsa' || key.asymmetricKeyDetails.modulusLength !== 3072) throw new Error('Wiederherstellung benötigt RSA-3072');
|
||||
return key;
|
||||
}
|
||||
|
||||
function fingerprint(key) {
|
||||
return crypto.createHash('sha256').update(key.export({ type: 'spki', format: 'der' })).digest('base64url');
|
||||
}
|
||||
|
||||
function wrapBackupKey(key, id, pem) {
|
||||
const publicKey = recoveryPublicKey(pem);
|
||||
return {
|
||||
version: 1,
|
||||
keyId: fingerprint(publicKey),
|
||||
wrappedKey: crypto.publicEncrypt({ key: publicKey, oaepHash: 'sha256', padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepLabel: Buffer.from(`MHU-RECOVERY-V1:${id}`) }, Buffer.from(key)).toString('base64url')
|
||||
};
|
||||
}
|
||||
|
||||
function recoverBackupKey(id, record, privatePem) {
|
||||
const { parseOnlineBackupKey, restoreOnlineBackup } = require('./online-backup');
|
||||
if (!record?.recovery || record.recovery.version !== 1) throw new Error('Diese Sicherung enthält keine Wiederherstellungsdaten');
|
||||
if (record.expiresAt !== null && (!record.expiresAt || !Number.isFinite(Date.parse(record.expiresAt)) || Date.parse(record.expiresAt) <= Date.now())) throw new Error('Die Sicherung ist abgelaufen oder ihr Ablaufdatum ist ungültig');
|
||||
const privateKey = crypto.createPrivateKey(privatePem);
|
||||
const publicKey = crypto.createPublicKey(privateKey);
|
||||
if (record.recovery.keyId !== fingerprint(publicKey)) throw new Error('Der Wiederherstellungsschlüssel gehört nicht zu dieser Sicherung');
|
||||
const wrapped = record.recovery.wrappedKey;
|
||||
if (typeof wrapped !== 'string' || !/^[A-Za-z0-9_-]{512}$/.test(wrapped)) throw new Error('Ungültige Wiederherstellungsdaten');
|
||||
const key = crypto.privateDecrypt({ key: privateKey, oaepHash: 'sha256', padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepLabel: Buffer.from(`MHU-RECOVERY-V1:${id}`) }, Buffer.from(wrapped, 'base64url')).toString('utf8');
|
||||
if (parseOnlineBackupKey(key).id !== id) throw new Error('Die Sicherungskennung stimmt nicht überein');
|
||||
restoreOnlineBackup(key, record.blob);
|
||||
return key;
|
||||
}
|
||||
|
||||
module.exports = { recoveryPublicKey, wrapBackupKey, recoverBackupKey };
|
||||
+12
-2
@@ -156,7 +156,7 @@ function onlineBackupExpiration(exportedAt, retention) {
|
||||
return new Date(created.getTime() + seconds * 1000).toISOString();
|
||||
}
|
||||
|
||||
function createOnlineBackup(settings, appVersion, exportedAt = new Date().toISOString(), retention = DEFAULT_ONLINE_BACKUP_RETENTION) {
|
||||
function createOnlineBackup(settings, appVersion, exportedAt = new Date().toISOString(), retention = DEFAULT_ONLINE_BACKUP_RETENTION, recoveryPublicKey = null) {
|
||||
const normalizedRetention = normalizeOnlineBackupRetention(retention);
|
||||
const expiresInSeconds = ONLINE_BACKUP_RETENTION_SECONDS[normalizedRetention];
|
||||
const expiresAt = onlineBackupExpiration(exportedAt, normalizedRetention);
|
||||
@@ -192,7 +192,8 @@ function createOnlineBackup(settings, appVersion, exportedAt = new Date().toISOS
|
||||
id: parsed.id,
|
||||
blob: blobBytes.toString('base64url'),
|
||||
deleteVerifier,
|
||||
expiresInSeconds
|
||||
expiresInSeconds,
|
||||
...(recoveryPublicKey ? { recovery: require('./online-backup-recovery').wrapBackupKey(key, parsed.id, recoveryPublicKey) } : {})
|
||||
},
|
||||
expiresAt
|
||||
};
|
||||
@@ -228,6 +229,14 @@ function restoreOnlineBackup(key, blob) {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadRecoveryPublicKey(baseUrl = ONLINE_BACKUP_API_URL, options) {
|
||||
const { response, body } = await requestText(endpoint(baseUrl, '/v1/recovery-key'), { method: 'GET', headers: { accept: 'application/json' } }, options);
|
||||
if (response.status !== 200) throw new Error('Wiederherstellungsschlüssel des Sicherungsdienstes ist nicht verfügbar');
|
||||
const value = JSON.parse(body);
|
||||
require('./online-backup-recovery').recoveryPublicKey(value.publicKey);
|
||||
return value.publicKey;
|
||||
}
|
||||
|
||||
async function uploadOnlineBackup(record, baseUrl = ONLINE_BACKUP_API_URL, options) {
|
||||
const { response } = await requestText(endpoint(baseUrl, '/v1/backups'), {
|
||||
method: 'POST',
|
||||
@@ -277,6 +286,7 @@ module.exports = {
|
||||
createOnlineBackup,
|
||||
deleteOnlineBackup,
|
||||
downloadOnlineBackup,
|
||||
downloadRecoveryPublicKey,
|
||||
normalizeOnlineBackupRetention,
|
||||
onlineBackupExpiration,
|
||||
parseOnlineBackupKey,
|
||||
|
||||
Reference in New Issue
Block a user