Master Password…): set one, change it (the current one first), remove it. * Closes with ['keyring' => 'unlocked' | 'changed' | 'removed' | 'forgotten']: MainForm reconnects the saved servers. * The fields are password fields; what is typed is never shown, logged or kept. * * @property IconBox $icon * @property Label $heading * @property Label $message * @property Container $currentPanel * @property Label $currentLabel * @property TextBox $current * @property Container $newPanel * @property TextBox $newPassword * @property TextBox $confirm * @property Label $note * @property Label $error * @property Button $forget * @property Button $remove * @property Button $ok * @property Button $cancel */ final class MasterPasswordForm extends Form { public function __construct(private Session $session) { parent::__construct(); } /** XAPI's answers, events and the console go on while this dialog is open. */ protected function tick(float $dt): void { $this->session->tick($dt); } private string $mode = 'manage'; /** At start (MainForm::loaded): asks the master password when saved passwords wait for it. True when it was asked. */ public static function askAtStart(MainForm $form): bool { if (!$form->session->keyring()->locked($form->session)) { return false; } Engine::get()->scenes->push(self::class, ['mode' => 'unlock']); return true; } protected function loaded(array $params = []): void { $this->mode = (string)($params['mode'] ?? 'manage'); $keyring = $this->session->keyring(); $master = $keyring->hasMaster(); $unlock = $this->mode === 'unlock'; $this->current->text = $this->newPassword->text = $this->confirm->text = ''; $this->error->text = ''; $this->currentPanel->visible = $master; $this->newPanel->visible = !$unlock; $this->forget->visible = $master; $this->remove->visible = !$unlock && $master; $this->icon->kind = $unlock ? 'warning' : 'info'; $this->heading->text = $unlock ? 'Enter your master password' : 'Master Password'; $this->currentLabel->text = $unlock ? 'Master password:' : 'Current master password:'; $this->message->text = match (true) { $unlock => 'Your saved server passwords are protected by a master password. Enter it to reconnect your servers.', $master => 'A master password protects your saved server passwords. Change it or remove it here.', default => 'Set a master password to protect the passwords XenCenter remembers: it is asked once each time XenCenter starts.', }; $this->note->text = $master ? 'Encrypted with ' . $keyring->describe() . '. A forgotten master password cannot be recovered: the saved passwords are then lost (not the servers).' : 'Without a master password the saved passwords are still encrypted (' . $keyring->describe() . '), but this only protects servers.json against casual reading: anyone who can read your files can read the key too.'; $this->ok->text = $unlock ? 'Unlock' : ($master ? 'Change' : 'Set'); $this->cancel->text = $unlock ? 'Not Now' : 'Cancel'; $this->setFocus($master ? $this->current : $this->newPassword); } public function onOkClick(mixed $sender = null): void { $keyring = $this->session->keyring(); if ($this->mode === 'unlock') { if ($keyring->unlockSession($this->session, $this->current->text)) { $this->close(['keyring' => 'unlocked']); return; } $this->wrong(); return; } if (!$this->checkCurrent()) { return; } if ($this->newPassword->text === '') { $this->error->text = 'Enter the new master password.'; $this->setFocus($this->newPassword); return; } if ($this->newPassword->text !== $this->confirm->text) { $this->error->text = 'The two new passwords are not the same.'; $this->confirm->text = ''; $this->setFocus($this->confirm); return; } $keyring->changeMaster($this->session, $this->newPassword->text); $this->close(['keyring' => 'changed']); } public function onRemoveClick(Button $sender): void { if ($this->checkCurrent()) { $this->session->keyring()->changeMaster($this->session, null); $this->close(['keyring' => 'removed']); } } public function onForgetClick(Button $sender): void { $this->session->keyring()->forgetAll($this->session); $this->close(['keyring' => 'forgotten']); } public function onCancelClick(Button $sender): void { $this->close(); } /** With a master password, the current one opens the keyring first. */ private function checkCurrent(): bool { $keyring = $this->session->keyring(); if (!$keyring->hasMaster() || $keyring->unlockSession($this->session, $this->current->text)) { return true; } $this->wrong(); return false; } private function wrong(): void { $this->error->text = 'The master password is wrong. Try again' . ($this->mode === 'unlock' ? ', or forget the saved passwords.' : '.'); $this->current->text = ''; $this->setFocus($this->current); } }