Files
nextcloud-ng_backup/lib/Command/TargetTrustCurrent.php
T
dillard 08b916cac8 M5: chained catalog against rollback and hidden snapshots
Every change to the set of snapshots or trash entries writes an
encrypted catalog generation bound to the repository and its position,
with the hash of the previous generation. The server anchors the newest
generation it has seen in its app config; on open, the location must
show at least that generation and chain back to it, and every snapshot
listed must exist. A rolled-back or tampered location is refused (the
run fails and administrators are notified). Opening with the passphrase
on a new server accepts the newest valid chain.

occ backup:target:trust-current accepts a legitimately older state
(e.g. restored from the provider's snapshot) after typing the location
name; audited and announced to all administrators.

Tests: chain advances, rollback, hidden snapshot, swapped generation,
disaster recovery without anchor. Tested on NC34 by deleting the newest
catalog generation on the SFTP location.
2026-10-02 11:30:42 +02:00

58 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: 2026 Dillard Blom
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\NgBackup\Command;
use OCA\NgBackup\Db\AppConfigCatalogAnchor;
use OCA\NgBackup\Service\AlertService;
use OCA\NgBackup\Service\TargetService;
use OCP\IAppConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
/**
* Accept the state a location shows now, after NG Backup refused it as rolled back. Only for
* legitimate cases (e.g. the location was restored from the provider's own snapshot), only from
* occ, audited and announced to all administrators.
*/
class TargetTrustCurrent extends Command {
public function __construct(
private TargetService $targets,
private IAppConfig $appConfig,
private AlertService $alerts,
) {
parent::__construct();
}
protected function configure(): void {
$this->setName('backup:target:trust-current')
->setDescription('Accept the current state of a location that was refused as rolled back')
->addArgument('target', InputArgument::REQUIRED, 'Location name or id')
->setHelp("NG Backup refuses a location whose backup catalog is older than what this server saw\n"
. "before, because an attacker could have restored an old state. If you restored the location\n"
. "yourself (e.g. from the provider's snapshot), accept it with this command.");
}
protected function execute(InputInterface $input, OutputInterface $output): int {
$t = $this->targets->get($input->getArgument('target'));
$output->writeln('<comment>Only continue if you know why the location shows an older state.</comment>');
$answer = (string)$this->getHelper('question')->ask($input, $output, new Question('Type the location name to accept its current state: '));
if ($answer !== $t->getName()) {
$output->writeln('<error>Not confirmed.</error>');
return 1;
}
$this->appConfig->deleteKey('ng_backup', 'catalog_' . preg_replace('/[^a-f0-9]/', '', (string)$t->getRepositoryId()));
$repo = $this->targets->repository($t); // re-anchors on the newest valid catalog
$this->alerts->securityEvent('catalog_trusted', ['target' => $t->getName(), 'generation' => (string)$repo->catalogGeneration()]);
$output->writeln(sprintf('Accepted generation %d of %s (%d snapshots).', $repo->catalogGeneration(), $t->getName(), count($repo->listSnapshots())));
return 0;
}
}