Passphrases of at least 16 characters; explain that the location holds the key too
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+5
-2
@@ -12,13 +12,16 @@ occ backup:key:kit --output=/var/tmp/ng_backup-kit.json
|
||||
occ backup:key:confirm -u <your-admin-uid> --code=<code shown> --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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<!-- 1. Create the key -->
|
||||
<form v-if="!status.key.initialized" class="ngb-form" @submit.prevent="createKey">
|
||||
<NcTextField v-model="label" :label="t('ng_backup', 'Who keeps this passphrase (e.g. Safe, CTO)')" />
|
||||
<NcPasswordField v-model="pass1" :label="t('ng_backup', 'Passphrase (at least 12 characters)')" autocomplete="new-password" />
|
||||
<NcPasswordField v-model="pass1" :label="t('ng_backup', 'Passphrase (at least 16 characters)')" autocomplete="new-password" />
|
||||
<NcPasswordField v-model="pass2" :label="t('ng_backup', 'Repeat passphrase')" autocomplete="new-password"
|
||||
:error="pass2 !== '' && pass1 !== pass2" :helper-text="pass2 !== '' && pass1 !== pass2 ? t('ng_backup', 'The passphrases do not match') : ''" />
|
||||
<NcTextField v-model="delay" type="number" min="1" max="365"
|
||||
:label="t('ng_backup', 'Days a removed backup stays in the trash before it is really deleted')"
|
||||
:helper-text="t('ng_backup', 'Protects against a malicious or mistaken cleanup. Cannot be changed later.')" />
|
||||
<NcButton type="submit" variant="primary" :disabled="busy || pass1.length < 12 || pass1 !== pass2 || !(delay >= 1 && delay <= 365)">
|
||||
<NcButton type="submit" variant="primary" :disabled="busy || pass1.length < 16 || pass1 !== pass2 || !(delay >= 1 && delay <= 365)">
|
||||
{{ t('ng_backup', 'Create backup key') }}
|
||||
</NcButton>
|
||||
</form>
|
||||
@@ -46,10 +46,10 @@
|
||||
<NcButton v-if="!slotForm && status.key.slots.length < status.key.maxSlots" @click="openSlotForm(null)">{{ t('ng_backup', 'Add passphrase') }}</NcButton>
|
||||
<form v-if="slotForm" class="ngb-form" @submit.prevent="saveSlot">
|
||||
<NcTextField v-model="slotForm.label" :label="t('ng_backup', 'Who keeps this passphrase')" />
|
||||
<NcPasswordField v-model="slotForm.pass1" :label="t('ng_backup', 'Passphrase (at least 12 characters)')" autocomplete="new-password" />
|
||||
<NcPasswordField v-model="slotForm.pass1" :label="t('ng_backup', 'Passphrase (at least 16 characters)')" autocomplete="new-password" />
|
||||
<NcPasswordField v-model="slotForm.pass2" :label="t('ng_backup', 'Repeat passphrase')" autocomplete="new-password" :error="slotForm.pass2 !== '' && slotForm.pass1 !== slotForm.pass2" />
|
||||
<div class="ngb-row">
|
||||
<NcButton type="submit" variant="primary" :disabled="busy || slotForm.pass1.length < 12 || slotForm.pass1 !== slotForm.pass2">{{ t('ng_backup', 'Save') }}</NcButton>
|
||||
<NcButton type="submit" variant="primary" :disabled="busy || slotForm.pass1.length < 16 || slotForm.pass1 !== slotForm.pass2">{{ t('ng_backup', 'Save') }}</NcButton>
|
||||
<NcButton @click="slotForm = null">{{ t('ng_backup', 'Cancel') }}</NcButton>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user