From 2e781ff3578e998546919ed09cfae412c839aa05 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 5 Nov 2020 10:59:31 +0100 Subject: [PATCH] Add MultifactorManageForm --- com.woltlab.wcf/page.xml | 15 +- com.woltlab.wcf/templates/accountSecurity.tpl | 10 +- .../templates/multifactorManage.tpl | 10 + .../templates/multifactorManageBackup.tpl | 8 + .../lib/form/MultifactorManageForm.class.php | 179 ++++++++++++++++++ .../BackupMultifactorMethod.class.php | 146 ++++++++++++++ .../multifactor/IMultifactorMethod.class.php | 22 +++ .../TotpMultifactorMethod.class.php | 13 ++ .../files/style/ui/accountSecurity.scss | 14 ++ wcfsetup/setup/db/install.sql | 13 ++ 10 files changed, 424 insertions(+), 6 deletions(-) create mode 100644 com.woltlab.wcf/templates/multifactorManage.tpl create mode 100644 com.woltlab.wcf/templates/multifactorManageBackup.tpl create mode 100644 wcfsetup/install/files/lib/form/MultifactorManageForm.class.php diff --git a/com.woltlab.wcf/page.xml b/com.woltlab.wcf/page.xml index 6c9fe24f85..0cd257e6f4 100644 --- a/com.woltlab.wcf/page.xml +++ b/com.woltlab.wcf/page.xml @@ -657,7 +657,6 @@ Dashboard Dashboard 1 - Dashboard dashboard @@ -669,7 +668,6 @@ Cookie Policy 1 0 - Cookie Policy This website uses cookies required to operate and use this site. Below you will find an explanation on our cookie usage.

@@ -729,7 +727,6 @@ Privacy Policy 1 0 - Privacy Policy 1. An overview of data protection @@ -831,8 +828,18 @@ E-Mail: [E-Mail-Adresse der verantwortlichen Stelle]


Verantwort datenschutzerklaerung + + system + wcf\form\MultifactorManageForm + Mehrfaktor-Authentifizierung einrichten + Manage Multifactor Authentication + 1 + com.woltlab.wcf.AccountSecurity + 1 + 1 + - + diff --git a/com.woltlab.wcf/templates/accountSecurity.tpl b/com.woltlab.wcf/templates/accountSecurity.tpl index ffd304234a..9684b731ae 100644 --- a/com.woltlab.wcf/templates/accountSecurity.tpl +++ b/com.woltlab.wcf/templates/accountSecurity.tpl @@ -12,8 +12,8 @@ -

-
+
+

{lang}wcf.user.security.multifactor.{$method->objectType}{/lang} @@ -26,6 +26,12 @@ {$method->getProcessor()->getStatusText($__wcf->user)}

+ +
{/foreach} diff --git a/com.woltlab.wcf/templates/multifactorManage.tpl b/com.woltlab.wcf/templates/multifactorManage.tpl new file mode 100644 index 0000000000..ad34602b3d --- /dev/null +++ b/com.woltlab.wcf/templates/multifactorManage.tpl @@ -0,0 +1,10 @@ +{capture assign='pageTitle'}{lang}wcf.user.security.multifactor.{$method->objectType}.manage{/lang}{/capture} +{capture assign='contentTitle'}{lang}wcf.user.security.multifactor.{$method->objectType}.manage{/lang}{/capture} + +{include file='userMenuSidebar'} + +{include file='header' __disableAds=true __sidebarLeftHasMenu=true} + +{@$form->getHtml()} + +{include file='footer' __disableAds=true} diff --git a/com.woltlab.wcf/templates/multifactorManageBackup.tpl b/com.woltlab.wcf/templates/multifactorManageBackup.tpl new file mode 100644 index 0000000000..1fff904d7a --- /dev/null +++ b/com.woltlab.wcf/templates/multifactorManageBackup.tpl @@ -0,0 +1,8 @@ +
    +{foreach from=$codes item='code'} +
  1. + {foreach from=$code[chunks] item='chunk'}{$chunk}{/foreach} + {if $code[useTime]}({$code[useTime]|plainTime}){/if} +
  2. +{/foreach} +
diff --git a/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php new file mode 100644 index 0000000000..36e83a4fd7 --- /dev/null +++ b/wcfsetup/install/files/lib/form/MultifactorManageForm.class.php @@ -0,0 +1,179 @@ + + * @package WoltLabSuite\Core\Form + * @since 5.4 + */ +class MultifactorManageForm extends AbstractFormBuilderForm { + /** + * @inheritDoc + */ + public $loginRequired = true; + + /** + * @inheritDoc + */ + public $formAction = 'setup'; + + /** + * @var ObjectType + */ + private $method; + + /** + * @var IMultifactorMethod + */ + private $processor; + + /** + * @var int + */ + private $setupId; + + /** + * @var mixed + */ + private $returnData; + + /** + * @inheritDoc + */ + public function readParameters() { + parent::readParameters(); + + if (!isset($_GET['id'])) { + throw new IllegalLinkException(); + } + + $objectType = ObjectTypeCache::getInstance()->getObjectType(intval($_GET['id'])); + + if (!$objectType) { + throw new IllegalLinkException(); + } + if ($objectType->getDefinition()->definitionName !== 'com.woltlab.wcf.multifactor') { + throw new IllegalLinkException(); + } + + $this->method = $objectType; + $this->processor = $this->method->getProcessor(); + + $sql = "SELECT setupID + FROM wcf".WCF_N."_user_multifactor + WHERE userID = ? + AND objectTypeID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + WCF::getUser()->userID, + $this->method->objectTypeID, + ]); + $this->setupId = $statement->fetchSingleColumn(); + } + + /** + * @inheritDoc + */ + protected function createForm() { + parent::createForm(); + + $this->processor->createManagementForm($this->form, $this->setupId, $this->returnData); + } + + public function save() { + AbstractForm::save(); + + WCF::getDB()->beginTransaction(); + + /** @var int|null $setupId */ + $setupId = null; + if ($this->setupId) { + $sql = "SELECT setupId + FROM wcf".WCF_N."_user_multifactor + WHERE setupId = ? + FOR UPDATE"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + $this->setupId, + ]); + + $setupId = intval($statement->fetchSingleColumn()); + } + else { + $sql = "INSERT INTO wcf".WCF_N."_user_multifactor + (userID, objectTypeID) + VALUES (?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ + WCF::getUser()->userID, + $this->method->objectTypeID, + ]); + + $setupId = intval(WCF::getDB()->getInsertID("wcf".WCF_N."_user_multifactor", 'setupID')); + } + + if (!$setupId) { + throw new \RuntimeException("Multifactor setup disappeared"); + } + + $this->returnData = $this->processor->processManagementForm($this->form, $setupId); + + $this->setupId = $setupId; + WCF::getDB()->commitTransaction(); + + $this->saved(); + } + + /** + * @inheritDoc + */ + public function saved() { + AbstractForm::saved(); + + $this->form->cleanup(); + $this->buildForm(); + + $this->form->showSuccessMessage(true); + } + + /** + * @inheritDoc + */ + protected function setFormAction() { + $this->form->action(LinkHandler::getInstance()->getControllerLink(static::class, [ + 'id' => $this->method->objectTypeID, + ])); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ + 'method' => $this->method, + ]); + } + + /** + * @inheritDoc + */ + public function show() { + UserMenu::getInstance()->setActiveMenuItem('wcf.user.menu.profile.security'); + + parent::show(); + } +} diff --git a/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php b/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php index ad4f32f215..ac3fd59627 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/BackupMultifactorMethod.class.php @@ -1,6 +1,16 @@ algorithm = new Bcrypt(); + } + /** * Returns the number of remaining codes. */ @@ -19,4 +41,128 @@ class BackupMultifactorMethod implements IMultifactorMethod { // TODO: Return a proper text. return random_int(10000, 99999)." codes remaining"; } + + /** + * @inheritDoc + */ + public function createManagementForm(IFormDocument $form, ?int $setupId, $returnData = null): void { + $form->addDefaultButton(false); + $form->successMessage('wcf.user.security.multifactor.backup.success'); + + if ($setupId) { + $sql = "SELECT * + FROM wcf".WCF_N."_user_multifactor_backup + WHERE setupID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$setupId]); + + $codes = $statement->fetchAll(\PDO::FETCH_ASSOC); + + $codes = array_map(function ($code) use ($returnData) { + if (isset($returnData[$code['identifier']])) { + $code['chunks'] = str_split($returnData[$code['identifier']], self::CHUNK_LENGTH); + } + else { + $code['chunks'] = [ + $code['identifier'], + ]; + + while (\count($code['chunks']) < self::CHUNKS) { + $code['chunks'][] = \str_repeat('x', self::CHUNK_LENGTH); + } + } + + return $code; + }, $codes); + + $statusContainer = FormContainer::create('existingCodesContainer') + ->label('wcf.user.security.multifactor.backup.existingCodes') + ->appendChildren([ + TemplateFormNode::create('existingCodes') + ->templateName('multifactorManageBackup') + ->variables([ + 'codes' => $codes, + ]), + ]); + $form->appendChild($statusContainer); + + $regenerateContainer = FormContainer::create('regenerateCodesContainer') + ->label('wcf.user.security.multifactor.backup.regenerateCodes') + ->appendChildren([ + ButtonFormField::create('regenerateCodes') + ->buttonLabel('wcf.user.security.multifactor.backup.regenerateCodes') + ->objectProperty('action') + ->value('regenerateCodes') + ->addValidator(new FormFieldValidator('regenerateCodes', function (ButtonFormField $field) { + if ($field->getValue() === null) { + $field->addValidationError(new FormFieldValidationError('unreachable', 'unreachable')); + } + })), + ]); + $form->appendChild($regenerateContainer); + } + else { + $generateContainer = FormContainer::create('generateCodesContainer') + ->label('wcf.user.security.multifactor.backup.generateCodes') + ->appendChildren([ + ButtonFormField::create('generateCodes') + ->buttonLabel('wcf.user.security.multifactor.backup.generateCodes') + ->objectProperty('action') + ->value('generateCodes') + ->addValidator(new FormFieldValidator('generateCodes', function (ButtonFormField $field) { + if ($field->getValue() === null) { + $field->addValidationError(new FormFieldValidationError('unreachable', 'unreachable')); + } + })), + ]); + $form->appendChild($generateContainer); + } + } + + /** + * @inheritDoc + */ + public function processManagementForm(IFormDocument $form, int $setupId): array { + $formData = $form->getData(); + assert($formData['action'] === 'generateCodes' || $formData['action'] === 'regenerateCodes'); + + $sql = "DELETE FROM wcf".WCF_N."_user_multifactor_backup + WHERE setupID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([$setupId]); + + $codes = []; + for ($i = 0; $i < 10; $i++) { + $chunks = []; + for ($part = 0; $part < self::CHUNKS; $part++) { + $chunks[] = \random_int( + pow(10, self::CHUNK_LENGTH - 1), + pow(10, self::CHUNK_LENGTH) - 1 + ); + } + + $identifier = $chunks[0]; + if (isset($codes[$identifier])) { + continue; + } + + $codes[$identifier] = implode('', $chunks); + } + + $sql = "INSERT INTO wcf".WCF_N."_user_multifactor_backup + (setupID, identifier, code, createTime) + VALUES (?, ?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $algorithName = PasswordAlgorithmManager::getInstance()->getNameFromAlgorithm($this->algorithm); + foreach ($codes as $identifier => $code) { + $statement->execute([ + $setupId, + $identifier, + $algorithName.':'.$this->algorithm->hash($code), + TIME_NOW, + ]); + } + + return $codes; + } } diff --git a/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php b/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php index 33ec715ccd..22abe11556 100644 --- a/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php +++ b/wcfsetup/install/files/lib/system/user/multifactor/IMultifactorMethod.class.php @@ -1,6 +1,7 @@