76 lines
3.2 KiB
JavaScript
76 lines
3.2 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
check
|
|
};
|
|
|
|
const assert = require('assert'),
|
|
backups = require('./backups.js'),
|
|
backupFormats = require('./backupformats.js'),
|
|
backupSites = require('./backupsites.js'),
|
|
BoxError = require('./boxerror'),
|
|
consumers = require('node:stream/consumers'),
|
|
crypto = require('node:crypto'),
|
|
debug = require('debug')('box:backupintegrity'),
|
|
safe = require('safetydance');
|
|
|
|
async function downloadBackupInfo(backupSite, backup) {
|
|
const stream = await backupSites.storageApi(backupSite).download(backupSite.config, `${backup.remotePath}.backupinfo`);
|
|
const buffer = await consumers.buffer(stream);
|
|
return buffer;
|
|
}
|
|
|
|
async function verify(backup, backupSite, progressCallback) {
|
|
assert.strictEqual(typeof backup, 'object');
|
|
assert.strictEqual(typeof backupSite, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
if (backup === null) return [`${backup.id} is missing in database`];
|
|
|
|
const [downloadError, backupInfoBuffer] = await safe(downloadBackupInfo(backupSite, backup));
|
|
if (downloadError) {
|
|
const messages = [`Failed to download ${backup.remotePath}.backupinfo: ${downloadError.message}`];
|
|
await backups.setIntegrityResult(backup, 'failed', { messages });
|
|
return;
|
|
}
|
|
|
|
const validSignature = crypto.verify(null /* algo */, backupInfoBuffer, backupSite.integrityKeyPair.publicKey, Buffer.from(backup.integrity.signature, 'hex'));
|
|
progressCallback({ message: `Signature valid? ${validSignature}`});
|
|
|
|
const backupInfo = JSON.parse(backupInfoBuffer.toString('utf8'));
|
|
const integrityMap = new Map(Object.entries(backupInfo));
|
|
|
|
const [verifyError, verifyMessages] = await safe(backupFormats.api(backupSite.format).verify(backupSite, backup.remotePath, integrityMap, progressCallback));
|
|
progressCallback({ message: 'Verification done' });
|
|
|
|
const messages = [];
|
|
if (!validSignature) messages.push(`${backup.remotePath}.backupinfo has invalid signature`);
|
|
if (verifyError) messages.push(`Failed to verify ${backup.remotePath}: ${verifyError.message}`);
|
|
if (verifyMessages) messages.push(...verifyMessages);
|
|
|
|
debug(`verified: ${JSON.stringify(verifyMessages, null, 4)}`);
|
|
|
|
return messages;
|
|
}
|
|
|
|
async function check(backupId, progressCallback) {
|
|
const backup = await backups.get(backupId);
|
|
if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'Backup not found');
|
|
|
|
const backupSite = await backupSites.get(backup.siteId);
|
|
if (!backupSite) throw new BoxError(BoxError.BAD_FIELD, 'Backup site not found');
|
|
|
|
const aggregatedMessages = [];
|
|
for (const depId of backup.dependsOn) {
|
|
const depBackup = await backups.get(depId);
|
|
const messages = await verify(depBackup, backupSite, progressCallback);
|
|
|
|
await backups.setIntegrityResult(backup, messages.length === 0 ? 'passed' : 'failed', { messages });
|
|
if (messages.length) aggregatedMessages.push(`Integrity check of dependent backup ${depBackup.remotePath} failed`);
|
|
}
|
|
|
|
const messages = await verify(backup, backupSite, progressCallback);
|
|
aggregatedMessages.push(...messages);
|
|
await backups.setIntegrityResult(backup, aggregatedMessages.length === 0 ? 'passed' : 'failed', { messages: aggregatedMessages });
|
|
}
|