From 629b8dd3157ffddd37a75020e3a38de48a5fc140 Mon Sep 17 00:00:00 2001 From: Dillard Blom Date: Fri, 16 Oct 2026 00:07:39 +0200 Subject: [PATCH] Passphrases of at least 16 characters; explain that the location holds the key too --- CHANGELOG.md | 4 ++++ README.md | 16 +++++++++++++--- docs/HOWTO.md | 7 +++++-- lib/Command/KeyInit.php | 2 +- lib/Command/KeySlots.php | 2 +- lib/Service/KeyService.php | 13 ++++++++----- src/components/KeySetup.vue | 8 ++++---- 7 files changed, 36 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f310b6..c25063b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,10 @@ instead carried by the app description, the git tag, the release, and this file. which location, setup and size you used, and how it went. ### Changed +- New passphrases must be at least 16 characters (was 12). The location stores the key + encrypted with each passphrase, so the passphrase is what really protects the backups. + Existing passphrases keep working. The README and the how-to now say so, and why the + recovery kit must still be kept off the server. - Dates and times in the web interface follow the locale in the user's Nextcloud settings instead of the browser's language, so a 24-hour locale shows 16:00 rather than 4:00 PM. - The list of backup runs (web interface and `occ backup:status`) shows the runs of the last 7 diff --git a/README.md b/README.md index c2ec128..b6a11e5 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,19 @@ All backups of an installation are encrypted with one master key. - The **server** keeps the master key, encrypted with the instance secret, so scheduled backups and restores work without anyone typing a passphrase. Any administrator can restore from the web interface. Consequence: whoever fully controls the server can read the backups; whoever - only gets hold of the backup location cannot. -- For a **restore on a new server** (disaster recovery) you need the **recovery kit** and **one - passphrase**. The kit contains the master key, encrypted separately with each passphrase. + only gets hold of the backup location cannot, as long as the passphrases are strong (see below). +- The **location holds the key too**, by design (like restic and Borg in repokey mode): its + `config` file contains the master key, encrypted separately with each passphrase (Argon2id), + the same as in the recovery kit. Without a passphrase it is of no use, but whoever can read the + location can try passphrases offline. **Your passphrase is therefore the actual key**: it must + be at least 16 characters; five or more random words are a good choice. +- The **recovery kit** is your own copy of that encrypted key. Still keep it off the server and + off the location: an attacker who gets write access to the location can damage or delete its + `config` file on purpose, and without the kit that makes every backup there unreadable. +- For a **restore on a new server** (disaster recovery) you currently need the **recovery kit** + and **one passphrase**. +- Planned: an option to not store the key on the location at all. Then the kit is the only copy, + and losing it means losing every backup; that option will come with an explicit warning. - Up to **three passphrases** ("slots") can open the key, so one person leaving or dying does not lock the company out. A suggested setup: diff --git a/docs/HOWTO.md b/docs/HOWTO.md index 4ad750f..3f46b45 100644 --- a/docs/HOWTO.md +++ b/docs/HOWTO.md @@ -12,13 +12,16 @@ occ backup:key:kit --output=/var/tmp/ng_backup-kit.json occ backup:key:confirm -u --code= --confirm="Yes, I confirm" ``` -- The passphrase protects the backup. Keep it somewhere other than this server. +- The passphrase protects the backup: the location stores the key encrypted with it, so whoever + can read the location can try to guess it. Use at least 16 characters (five or more random + words work well) and keep it somewhere other than this server. - Optional: extra passphrases for other people with `occ backup:key:slot:add --label="..."`. Any one passphrase is enough to restore. Removing a slot later does not revoke access for someone who kept an old kit and that passphrase (see "Known limitations" in the README). - The recovery kit (`/var/tmp/ng_backup-kit.json` above) is also required for disaster recovery. The web user writes it there; then move it off the server as root, next to the passphrase, and delete the - copy in `/var/tmp`. Without the kit and a passphrase, the backup cannot be read. + copy in `/var/tmp`. The location holds the same encrypted key, but keep the kit anyway: if + someone damages or deletes the key on the location, the kit is the only way back in. ## 2. Add a backup location diff --git a/lib/Command/KeyInit.php b/lib/Command/KeyInit.php index aad8aac..5051fc3 100644 --- a/lib/Command/KeyInit.php +++ b/lib/Command/KeyInit.php @@ -50,7 +50,7 @@ HELP); $pass = rtrim((string)@file_get_contents($file), "\r\n"); } else { $helper = $this->getHelper('question'); - $q = (new Question('Passphrase (min. 12 characters): '))->setHidden(true)->setHiddenFallback(false); + $q = (new Question('Passphrase (min. ' . \OCA\NgBackup\Service\KeyService::MIN_PASSPHRASE . ' characters): '))->setHidden(true)->setHiddenFallback(false); $pass = (string)$helper->ask($input, $output, $q); $again = (string)$helper->ask($input, $output, (new Question('Repeat passphrase: '))->setHidden(true)->setHiddenFallback(false)); if ($pass !== $again) { diff --git a/lib/Command/KeySlots.php b/lib/Command/KeySlots.php index 56deb92..aa59df7 100644 --- a/lib/Command/KeySlots.php +++ b/lib/Command/KeySlots.php @@ -88,7 +88,7 @@ HELP; return rtrim((string)@file_get_contents($file), "\r\n"); } $h = $this->getHelper('question'); - $p = (string)$h->ask($input, $output, (new Question('Passphrase (min. 12 characters): '))->setHidden(true)->setHiddenFallback(false)); + $p = (string)$h->ask($input, $output, (new Question('Passphrase (min. ' . \OCA\NgBackup\Service\KeyService::MIN_PASSPHRASE . ' characters): '))->setHidden(true)->setHiddenFallback(false)); if ($p !== (string)$h->ask($input, $output, (new Question('Repeat: '))->setHidden(true)->setHiddenFallback(false))) { throw new \InvalidArgumentException('The passphrases do not match'); } diff --git a/lib/Service/KeyService.php b/lib/Service/KeyService.php index fca1d40..cf5c832 100644 --- a/lib/Service/KeyService.php +++ b/lib/Service/KeyService.php @@ -31,6 +31,11 @@ final class KeyService { private const KEY_KIT_VERSION = 'recovery_kit_version'; private const KEY_KIT_CODE = 'recovery_kit_code'; public const DEFAULT_DELETE_DELAY = 7; + /** + * The location holds the passphrase-wrapped key too, so whoever can read it can try + * passphrases offline: the passphrase is what really protects the backups. + */ + public const MIN_PASSPHRASE = 16; private ?KeyRing $cached = null; @@ -56,9 +61,7 @@ final class KeyService { if ($this->isInitialized()) { throw new \RuntimeException('A backup key already exists'); } - if (strlen($passphrase) < 12) { - throw new \InvalidArgumentException('The passphrase must be at least 12 characters'); - } + self::checkPassphrase($passphrase); $keys = KeyRing::generate(); $this->appConfig->setValueString(Application::APP_ID, self::KEY_MASTER, $this->crypto->encrypt(base64_encode($keys->exportMasterKey())), true, true); @@ -234,8 +237,8 @@ final class KeyService { } private static function checkPassphrase(string $passphrase): void { - if (strlen($passphrase) < 12) { - throw new \InvalidArgumentException('The passphrase must be at least 12 characters'); + if (strlen($passphrase) < self::MIN_PASSPHRASE) { + throw new \InvalidArgumentException('The passphrase must be at least ' . self::MIN_PASSPHRASE . ' characters'); } } diff --git a/src/components/KeySetup.vue b/src/components/KeySetup.vue index 24ab60f..08549e9 100644 --- a/src/components/KeySetup.vue +++ b/src/components/KeySetup.vue @@ -8,13 +8,13 @@
- + - + {{ t('ng_backup', 'Create backup key') }} @@ -46,10 +46,10 @@ {{ t('ng_backup', 'Add passphrase') }}
- +
- {{ t('ng_backup', 'Save') }} + {{ t('ng_backup', 'Save') }} {{ t('ng_backup', 'Cancel') }}