action) { 'add' => $this->setName('backup:key:slot:add')->setDescription('Add a passphrase slot') ->addOption('label', 'l', InputOption::VALUE_REQUIRED, 'Who or where, e.g. "CTO" or "Safe"', 'Slot'), 'replace' => $this->setName('backup:key:slot:replace')->setDescription('Set a new passphrase for a slot (e.g. its holder left)') ->addArgument('slot', InputArgument::REQUIRED, 'Slot number (see backup:key:slots)') ->addOption('label', 'l', InputOption::VALUE_REQUIRED, 'New label'), 'remove' => $this->setName('backup:key:slot:remove')->setDescription('Remove a passphrase slot (not the last one)') ->addArgument('slot', InputArgument::REQUIRED, 'Slot number (see backup:key:slots)'), default => $this->setName('backup:key:slots')->setDescription('List the passphrase slots of the backup key'), }; if ($this->action === 'add' || $this->action === 'replace') { $this->addOption('passphrase-file', null, InputOption::VALUE_REQUIRED, 'Read the passphrase from this file'); } $this->setHelp($help); } protected function execute(InputInterface $input, OutputInterface $output): int { if (!$this->keys->isInitialized()) { $output->writeln('No backup key yet: run occ backup:key:init first.'); return 1; } try { switch ($this->action) { case 'add': $n = $this->slots->add($this->passphrase($input, $output), (string)$input->getOption('label')); $output->writeln("Slot $n added."); break; case 'replace': $this->slots->replace((int)$input->getArgument('slot'), $this->passphrase($input, $output), $input->getOption('label')); $output->writeln('Slot ' . (int)$input->getArgument('slot') . ' has a new passphrase.'); break; case 'remove': $this->slots->remove((int)$input->getArgument('slot')); $output->writeln('Slot ' . (int)$input->getArgument('slot') . ' removed.'); break; default: (new Table($output))->setHeaders(['slot', 'label', 'set on']) ->setRows(array_map(fn ($s) => [$s['slot'], $s['label'], $s['created'] ? date('Y-m-d H:i', $s['created']) : ''], $this->keys->slots()))->render(); return 0; } } catch (\Throwable $e) { $output->writeln('' . $e->getMessage() . ''); return 1; } $output->writeln('Now download and confirm the new recovery kit: occ backup:key:kit -o FILE && occ backup:key:confirm'); return 0; } private function passphrase(InputInterface $input, OutputInterface $output): string { if ($file = $input->getOption('passphrase-file')) { 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)); if ($p !== (string)$h->ask($input, $output, (new Question('Repeat: '))->setHidden(true)->setHiddenFallback(false))) { throw new \InvalidArgumentException('The passphrases do not match'); } return $p; } }