Review fixes: the longest deletion delay when the location's settings cannot be read, a damaged passphrase slot does not stop the next one
CI / unit (8.2) (push) Has been cancelled
CI / unit (8.3) (push) Has been cancelled
CI / unit (8.4) (push) Has been cancelled
CI / psalm (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / integration (mariadb, 32) (push) Has been cancelled
CI / integration (mariadb, 33) (push) Has been cancelled
CI / integration (mariadb, 34) (push) Has been cancelled
CI / integration (mariadb, 35) (push) Has been cancelled
CI / integration (pgsql, 32) (push) Has been cancelled
CI / integration (pgsql, 33) (push) Has been cancelled
CI / integration (pgsql, 34) (push) Has been cancelled
CI / integration (pgsql, 35) (push) Has been cancelled
CI / integration (sqlite, 32) (push) Has been cancelled
CI / integration (sqlite, 33) (push) Has been cancelled
CI / integration (sqlite, 34) (push) Has been cancelled
CI / integration (sqlite, 35) (push) Has been cancelled
CI / integration-targets (push) Has been cancelled
CI / integration-user (32) (push) Has been cancelled
CI / integration-user (33) (push) Has been cancelled
CI / integration-user (34) (push) Has been cancelled
CI / integration-user (35) (push) Has been cancelled
CI / integration-full-restore (push) Has been cancelled
CI / unit (8.2) (push) Has been cancelled
CI / unit (8.3) (push) Has been cancelled
CI / unit (8.4) (push) Has been cancelled
CI / psalm (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / integration (mariadb, 32) (push) Has been cancelled
CI / integration (mariadb, 33) (push) Has been cancelled
CI / integration (mariadb, 34) (push) Has been cancelled
CI / integration (mariadb, 35) (push) Has been cancelled
CI / integration (pgsql, 32) (push) Has been cancelled
CI / integration (pgsql, 33) (push) Has been cancelled
CI / integration (pgsql, 34) (push) Has been cancelled
CI / integration (pgsql, 35) (push) Has been cancelled
CI / integration (sqlite, 32) (push) Has been cancelled
CI / integration (sqlite, 33) (push) Has been cancelled
CI / integration (sqlite, 34) (push) Has been cancelled
CI / integration (sqlite, 35) (push) Has been cancelled
CI / integration-targets (push) Has been cancelled
CI / integration-user (32) (push) Has been cancelled
CI / integration-user (33) (push) Has been cancelled
CI / integration-user (34) (push) Has been cancelled
CI / integration-user (35) (push) Has been cancelled
CI / integration-full-restore (push) Has been cancelled
- Unreadable settings or an invalid delay on the location: 365 days, so nothing leaves the Bin sooner than on the old server - A passphrase change that still cannot reach a location is logged; removing a location clears its flag - A damaged slot (missing parts, invalid base64) is skipped and the next slot is tried - Tests: damaged settings scenario, damaged slot, upgrade test checks the migration and the time zone
This commit is contained in:
@@ -164,7 +164,7 @@ HELP);
|
||||
$days = 'Deletion delay for forgotten snapshots: <info>' . $this->keys->deleteDelayDays() . '</info> days';
|
||||
$output->writeln(match (true) {
|
||||
($r['deleteDelayDays'] ?? null) !== null => $days . ' (from the location).',
|
||||
$r['settingsUnreadable'] ?? false => '<comment>' . $days . ' (the default: the settings file on the location could not be read, it may be damaged). If this server used a longer delay, removed restore points leave the Bin sooner than before.</comment>',
|
||||
$r['settingsUnreadable'] ?? false => '<comment>' . $days . ' (the longest: the settings on the location could not be read or hold no valid delay, so nothing leaves the Bin sooner than on the old server; removed restore points take space for that long).</comment>',
|
||||
default => $days . ' (the default: the location has none stored, its backups were made before NG Backup 1.1.0).',
|
||||
});
|
||||
$output->writeln('Next: <info>occ backup:list</info> and <info>occ backup:restore:full</info>. Before backups run again, download a new recovery kit and confirm it (<info>occ backup:key:kit</info>, <info>occ backup:key:confirm</info>).');
|
||||
|
||||
@@ -87,14 +87,20 @@ final class KeyRing {
|
||||
foreach ($field['slots'] as $slot) {
|
||||
try {
|
||||
return self::unwrap($slot['key'], $passphrase);
|
||||
} catch (CryptoException) {
|
||||
} catch (CryptoException|\SodiumException|\TypeError|\ValueError) {
|
||||
// a wrong passphrase for this slot, or a damaged slot: the next one may still open
|
||||
}
|
||||
}
|
||||
throw new CryptoException('Wrong passphrase or damaged key');
|
||||
}
|
||||
|
||||
/** @param array{v:int, salt:string, ops:int, mem:int, nonce:string, key:string} $wrapped */
|
||||
/** @param array<string, mixed> $wrapped from a kit file or a location's config: checked here, not trusted */
|
||||
public static function unwrap(array $wrapped, #[\SensitiveParameter] string $passphrase): self {
|
||||
if (!isset($wrapped['salt'], $wrapped['nonce'], $wrapped['key'], $wrapped['ops'], $wrapped['mem'])
|
||||
|| !is_string($wrapped['salt']) || !is_string($wrapped['nonce']) || !is_string($wrapped['key'])
|
||||
|| !is_int($wrapped['ops']) || !is_int($wrapped['mem'])) {
|
||||
throw new CryptoException('Damaged key file: a part of the key is missing');
|
||||
}
|
||||
$b64 = static fn (string $s): string => sodium_base642bin($s, SODIUM_BASE64_VARIANT_ORIGINAL);
|
||||
// ops/mem come from unauthenticated JSON (kit file, repository config): bound them, so a
|
||||
// damaged or crafted file cannot make the KDF use unbounded memory or time.
|
||||
|
||||
@@ -27,6 +27,9 @@ use OCP\Server;
|
||||
* classes but its enabled/disabled state is left as the admin set it.
|
||||
*/
|
||||
final class TargetService {
|
||||
/** Deletion delay taken when a location's own cannot be read: nothing leaves the Bin sooner than on the old server. */
|
||||
public const LONGEST_DELETE_DELAY = 365;
|
||||
|
||||
public function __construct(
|
||||
private TargetMapper $mapper,
|
||||
private KeyService $keys,
|
||||
@@ -195,13 +198,16 @@ final class TargetService {
|
||||
throw new \RuntimeException('The key in the config at this location does not open its backups: the config is damaged or was replaced. Only the recovery kit can help now: occ backup:key:import <kit file>, then this command again.');
|
||||
}
|
||||
$settings = Repository::settingsAt($storageBackend, $keys);
|
||||
$delay = $settings['delete_delay_days'] ?? null;
|
||||
$delay = is_int($delay) ? $delay : null;
|
||||
$this->keys->adoptKey($keys, $wrapped, $delay, $replaceUnusedKey);
|
||||
$stored = $settings['delete_delay_days'] ?? null;
|
||||
$delay = is_int($stored) && $stored >= 1 && $stored <= 365 ? $stored : null;
|
||||
// Settings that are there but cannot be read, or hold no valid delay (also none at all): the old server's delay
|
||||
// is unknown, so take the longest one; a shorter guess could empty the Bin too soon.
|
||||
$unknown = ($settings['unreadable'] ?? false) === true || ($settings !== [] && $delay === null);
|
||||
$this->keys->adoptKey($keys, $wrapped, $delay ?? ($unknown ? self::LONGEST_DELETE_DELAY : null), $replaceUnusedKey);
|
||||
if ($old !== null) {
|
||||
$this->alerts->securityEvent('key_replaced', ['old' => $old, 'new' => $this->keys->fingerprint()]);
|
||||
}
|
||||
return $this->add($name, $backend, $auth, $options, $basePath, $progress) + ['deleteDelayDays' => $delay, 'settingsUnreadable' => ($settings['unreadable'] ?? false) === true];
|
||||
return $this->add($name, $backend, $auth, $options, $basePath, $progress) + ['deleteDelayDays' => $delay, 'settingsUnreadable' => $unknown];
|
||||
}
|
||||
|
||||
/** A passphrase change that missed this location (offline at the time) reaches it now. A failure is tried again at the next backup. */
|
||||
@@ -212,7 +218,9 @@ final class TargetService {
|
||||
try {
|
||||
$repo->updateWrappedKey($this->keys->wrappedKey());
|
||||
$this->keys->setWrappedKeyPending($target->getId(), false);
|
||||
} catch (\Throwable) {
|
||||
} catch (\Throwable $e) {
|
||||
Server::get(\Psr\Log\LoggerInterface::class)->warning('NG Backup: the last passphrase change still does not reach ' . $target->getName()
|
||||
. ' (it keeps the previous passphrases); tried again at its next backup: ' . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,6 +450,7 @@ final class TargetService {
|
||||
/** Remove the target from NG Backup. The backups on the target itself are not touched. */
|
||||
public function remove(Target $target): void {
|
||||
$this->mapper->delete($target);
|
||||
$this->keys->setWrappedKeyPending($target->getId(), false);
|
||||
// The cached index of its repository is only of use while a location points at it
|
||||
$repositoryId = $target->getRepositoryId();
|
||||
if ($repositoryId !== null && !array_filter($this->list(), fn (Target $t) => $t->getRepositoryId() === $repositoryId)) {
|
||||
|
||||
Reference in New Issue
Block a user