diff --git a/src/Application/Controller/Admin/d3user_totp.php b/src/Application/Controller/Admin/d3user_totp.php index 49cc164..7d30741 100644 --- a/src/Application/Controller/Admin/d3user_totp.php +++ b/src/Application/Controller/Admin/d3user_totp.php @@ -16,12 +16,18 @@ namespace D3\Totp\Application\Controller\Admin; use D3\Totp\Application\Model\d3totp; +use D3\Totp\Modules\Application\Model\d3_totp_user; use Doctrine\DBAL\DBALException; use OxidEsales\Eshop\Application\Controller\Admin\AdminDetailsController; +use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; +use OxidEsales\Eshop\Core\Exception\StandardException; +use OxidEsales\Eshop\Core\Registry; class d3user_totp extends AdminDetailsController { + protected $_sSaveError = null; + protected $_sThisTemplate = 'd3user_totp.tpl'; /** @@ -33,18 +39,62 @@ class d3user_totp extends AdminDetailsController { parent::render(); - $soxId = $this->_aViewData["oxid"] = $this->getEditObjectId(); + $soxId = $this->getEditObjectId(); + if (isset($soxId) && $soxId != "-1") { - /** @var d3totp $oTotp */ - $oTotp = oxNew(d3totp::class); - $oTotp->loadByUserId($soxId); - $this->_aViewData["edit"] = $oTotp; + /** @var d3_totp_user $oUser */ + $oUser = oxNew(User::class); + if ($oUser->load($soxId)) { + $this->addTplParam("oxid", $oUser->getId()); + } else { + $this->addTplParam("oxid", '-1'); + } + $this->addTplParam("edit", $oUser); } - if (!$this->_allowAdminEdit($soxId)) { - $this->_aViewData['readonly'] = true; + if ($this->_sSaveError) { + $this->addTplParam("sSaveError", $this->_sSaveError); } return $this->_sThisTemplate; } + + /** + * @throws \Exception + */ + public function save() + { + parent::save(); + + $aParams = Registry::getRequest()->getRequestEscapedParameter("editval"); + + try { + $pwd = Registry::getRequest()->getRequestEscapedParameter("pwd"); + + /** @var d3_totp_user $oUser */ + $oUser = oxNew(User::class); + if (false == $oUser->d3CheckPasswordPass($this->getEditObjectId(), $pwd)) { + $oException = oxNew(StandardException::class, 'EXCEPTION_USER_PASSWORDDONTPASS'); + throw $oException; + } + + /** @var d3totp $oTotp */ + $oTotp = oxNew(d3totp::class); + if ($aParams['d3totp__oxid']) { + $oTotp->load($aParams['d3totp__oxid']); + } else { + $aParams['d3totp__usetotp'] = 1; + $seed = Registry::getRequest()->getRequestEscapedParameter("secret"); + $otp = Registry::getRequest()->getRequestEscapedParameter("otp"); + + $oTotp->saveSecret($seed, $pwd); + $oTotp->assign($aParams); + $oTotp->verify($otp, $seed); + $oTotp->setId(); + } + $oTotp->save(); + } catch (\Exception $oExcp) { + $this->_sSaveError = $oExcp->getMessage(); + } + } } \ No newline at end of file diff --git a/src/Application/Model/d3totp.php b/src/Application/Model/d3totp.php index 3e4cfb3..72f6568 100644 --- a/src/Application/Model/d3totp.php +++ b/src/Application/Model/d3totp.php @@ -81,7 +81,8 @@ class d3totp extends BaseModel */ public function UserUseTotp() { - return $this->getFieldData('usetotp'); + return $this->getFieldData('usetotp') + && $this->getFieldData('seed'); } /** @@ -90,20 +91,24 @@ class d3totp extends BaseModel */ public function getSavedSecret() { - $secret = $this->getFieldData('seed'); + $seed_enc = $this->getFieldData('seed'); $sPwd = Registry::getSession()->getVariable('pwdTransmit'); - if ($secret) { - return $secret; + if ($seed_enc && $sPwd) { + $seed = $this->decrypt($seed_enc, $sPwd); + if ($seed) { + return $seed; + } } return null; } /** + * @param $seed * @return TOTP */ - public function getTotp() + public function getTotp($seed = null) { if (false == $this->totp) { @@ -113,10 +118,12 @@ class d3totp extends BaseModel $this->getUser()->getFieldData('oxusername') ? $this->getUser()->getFieldData('oxusername') : null, - $this->getSavedSecret() + $seed + ? $seed + : $this->getSavedSecret() ); } else { // version 0.9 (>= PHP 7.1) - $this->totp = TOTP::create($this->getSavedSecret()); + $this->totp = TOTP::create($seed ? $seed : $this->getSavedSecret()); $this->totp->setLabel($this->getUser()->getFieldData('oxusername') ? $this->getUser()->getFieldData('oxusername') : null @@ -151,17 +158,31 @@ class d3totp extends BaseModel */ public function getSecret() { - return $this->getTotp()->getSecret(); + return trim($this->getTotp()->getSecret()); + } + + /** + * @param $seed + * @param $key + */ + public function saveSecret($seed, $key) + { + $this->assign( + array( + 'seed' => $this->encrypt($seed, $key) + ) + ); } /** * @param $totp + * @param $seed * @return string * @throws d3totp_wrongOtpException */ - public function verify($totp) + public function verify($totp, $seed = null) { - $blVerify = $this->getTotp()->verify($totp, null, 2); + $blVerify = $this->getTotp($seed)->verify($totp, null, 2); if (false == $blVerify) { $oException = oxNew(d3totp_wrongOtpException::class, 'unvalid TOTP'); throw $oException; @@ -169,4 +190,43 @@ class d3totp extends BaseModel return $blVerify; } + + /** + * $key should have previously been generated in a cryptographically secure manner, e.g. via openssl_random_pseudo_bytes + * + * @param $plaintext + * @param $key + * @return string + */ + public function encrypt($plaintext, $key) + { + $ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC"); + $iv = openssl_random_pseudo_bytes($ivlen); + $ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv); + $hmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true); + return base64_encode($iv.$hmac.$ciphertext_raw); + } + + /** + * $key should have previously been generated in a cryptographically secure manner, e.g. via openssl_random_pseudo_bytes + * + * @param $ciphertext + * @param $key + * @return bool|string + */ + public function decrypt($ciphertext, $key) + { + $c = base64_decode($ciphertext); + $ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC"); + $iv = substr($c, 0, $ivlen); + $hmac = substr($c, $ivlen, $sha2len=32); + $ciphertext_raw = substr($c, $ivlen+$sha2len); + $original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv); + $calcmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true); + if (hash_equals($hmac, $calcmac)) { // PHP 5.6+ compute attack-safe comparison + return $original_plaintext; + } + + return false; + } } \ No newline at end of file diff --git a/src/Application/views/admin/blocks/d3totp_login_admin_login_form.tpl b/src/Application/views/admin/blocks/d3totp_login_admin_login_form.tpl index 409d79b..5462e85 100644 --- a/src/Application/views/admin/blocks/d3totp_login_admin_login_form.tpl +++ b/src/Application/views/admin/blocks/d3totp_login_admin_login_form.tpl @@ -12,6 +12,8 @@
[{oxmultilang ident="TOTP_INPUT_HELP"}] + + --Anmeldung abbrechen-- [{else}] [{$smarty.block.parent}] -[{/if}] +[{/if}] \ No newline at end of file diff --git a/src/Application/views/admin/de/d3totp_lang.php b/src/Application/views/admin/de/d3totp_lang.php index 1cde743..34f791f 100644 --- a/src/Application/views/admin/de/d3totp_lang.php +++ b/src/Application/views/admin/de/d3totp_lang.php @@ -25,9 +25,13 @@ $aLang = [ 'd3mxuser_totp' => '2-Faktor-Authentisierung', - 'D3_TOTP_ACTIVE' => 'Benutzer verwendet 2-Faktor-Authentisierung', - 'D3_TOTP_ACTIVE_HELP' => 'Benutzer verwendet 2-Faktor-Authentisierung', + 'D3_TOTP_REGISTERNEW' => 'neue Registrierung erstellen', 'D3_TOTP_QRCODE' => 'QR-Code', + 'D3_TOTP_QRCODE_HELP' => 'Scannen Sie diesen QR-Code mit Ihrer Authentisierungs-App, um dieses Benutzerkonto dort zu hinterlegen.', 'D3_TOTP_SECRET' => 'QR-Code kann nicht gescannt werden?', + 'D3_TOTP_SECRET_HELP' => 'Setzen Sie keine App ein, die den QR-Code scannen kann, können Sie diese Zeichenkette auch in Ihr Authentisierungstool kopieren. Stellen Sie bitte zusätzlich die Passwortlänge auf 6 Zeichen und das Zeitinterval auf 30 Sekunden ein.', + 'D3_TOTP_CURRPWD' => 'Anmeldepasswort des Benutzerkontos', + 'D3_TOTP_CURRPWD_HELP' => 'Die Zeichenkette wird verschlüsselt im Shop abgelegt. Zum Verschlüsseln wird das Passwort des ausgewählten Kundenkontos benötigt. Zugleich stellt dies sicher, dass nur Berechtigte Änderungen an diesen Einstellungen vornehmen dürfen.', 'D3_TOTP_CURROTP' => 'Bestätigung mit Einmalpasswort', + 'D3_TOTP_CURROTP_HELP' => 'Haben Sie dieses Kundenkonto in Ihrer Authentisierungs-App registriert, generieren Sie damit ein Einmalpasswort, tragen Sie es hier ein und senden das Formular direkt darauf hin ab.', ]; diff --git a/src/Application/views/admin/tpl/d3user_totp.tpl b/src/Application/views/admin/tpl/d3user_totp.tpl index 3acc096..f40e043 100644 --- a/src/Application/views/admin/tpl/d3user_totp.tpl +++ b/src/Application/views/admin/tpl/d3user_totp.tpl @@ -1,5 +1,7 @@ [{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}] +[{assign var="totp" value=$edit->d3GetTotp()}] + [{if $readonly}] [{assign var="readonly" value="readonly disabled"}] [{else}] @@ -17,80 +19,98 @@ + - - - -
- - [{block name="user_d3user_totp_form1"}] - - - - - [{/block}] + [{if $sSaveError}] +
- [{oxmultilang ident="D3_TOTP_ACTIVE"}] - - - getFieldData('usetotp') == 1}]checked[{/if}] [{$readonly}]> - [{oxinputhelp ident="D3_TOTP_ACTIVE_HELP"}] -
+ + + + +
[{oxmultilang ident=$sSaveError}]
+ [{/if}] -


- oxarticles__oxtitle->value && !$oxparentid}]disabled[{/if}] [{$readonly}]> - [{if $oxid!=-1 && !$readonly}] -     + [{if $oxid && $oxid != '-1'}] + + + - - - - -
+ + [{block name="user_d3user_totp_form1"}] + [{if false == $totp->getId()}] + + + + + + + + + + + + + + + + + + + + + + [{else}] + + + + [{/if}] - - -
+

[{oxmultilang ident="D3_TOTP_REGISTERNEW"}]

+
+ [{oxmultilang ident="D3_TOTP_QRCODE"}]  + + + [{oxinputhelp ident="D3_TOTP_QRCODE_HELP"}] +
+ + + + [{oxinputhelp ident="D3_TOTP_SECRET_HELP"}] +
+ + + + [{oxinputhelp ident="D3_TOTP_CURRPWD_HELP"}] +
+ + + + [{oxinputhelp ident="D3_TOTP_CURROTP_HELP"}] +
+ + + neuen Zugang anlegen, alle bisherigen Zugännge werden damit ungültig +
-
- - [{block name="user_d3user_totp_form2"}] + + [{/block}] - - - - - - - - - - - - [{/block}] -
- [{oxmultilang ident="D3_TOTP_QRCODE"}]  - - - [{* - - [{oxinputhelp ident="HELP_ARTICLE_MAIN_TITLE"}] - *}] +

+
- [{oxmultilang ident="D3_TOTP_SECRET"}]  - - [{$edit->getSecret()}] - [{* - - [{oxinputhelp ident="HELP_ARTICLE_MAIN_ARTNUM"}] - *}] -
- [{oxmultilang ident="D3_TOTP_CURROTP"}]  - - - [{oxinputhelp ident="D3_TOTP_CURROTP_HELP"}] -
-
+
+ + + + + [{block name="user_d3user_totp_form2"}][{/block}] +
+ + + + + [{/if}] [{include file="bottomnaviitem.tpl"}] diff --git a/src/Modules/Application/Controller/Admin/d3_totp_LoginController.php b/src/Modules/Application/Controller/Admin/d3_totp_LoginController.php index a70bc99..df0dec8 100644 --- a/src/Modules/Application/Controller/Admin/d3_totp_LoginController.php +++ b/src/Modules/Application/Controller/Admin/d3_totp_LoginController.php @@ -72,6 +72,7 @@ class d3_totp_LoginController extends d3_totp_LoginController_parent $return = parent::checklogin(); } elseif ($this->hasValidTotp($sTotp, $totp)) { Registry::getSession()->setVariable(d3totp::TOTP_SESSION_VARNAME, $sTotp); + Registry::getSession()->deleteVariable('pwdTransmit'); $return = "admin_start"; } } catch (d3totp_wrongOtpException $oEx) { diff --git a/src/Modules/Application/Model/d3_totp_user.php b/src/Modules/Application/Model/d3_totp_user.php index c59c53a..f0031fc 100644 --- a/src/Modules/Application/Model/d3_totp_user.php +++ b/src/Modules/Application/Model/d3_totp_user.php @@ -16,6 +16,9 @@ namespace D3\Totp\Modules\Application\Model; use D3\Totp\Application\Model\d3totp; +use Doctrine\DBAL\DBALException; +use OxidEsales\Eshop\Core\DatabaseProvider; +use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Registry; class d3_totp_user extends d3_totp_user_parent @@ -29,4 +32,51 @@ class d3_totp_user extends d3_totp_user_parent return $return; } + + /** + * @param $sUserId + * @param $sPassword + * @return bool + * @throws DatabaseConnectionException + */ + public function d3CheckPasswordPass($sUserId, $sPassword) + { + return (bool) DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->getOne( + $this->d3GetPasswordCheckQuery($sUserId, $sPassword) + ); + } + + /** + * @param $sUserId + * @param $sPassword + * @return string + * @throws DatabaseConnectionException + */ + public function d3GetPasswordCheckQuery($sUserId, $sPassword) + { + $oDb = \OxidEsales\Eshop\Core\DatabaseProvider::getDb(); + + $sUserSelect = "oxuser.oxid = " . $oDb->quote($sUserId); + + $sSalt = $oDb->getOne("SELECT `oxpasssalt` FROM `oxuser` WHERE " . $sUserSelect); + + $sPassSelect = " oxuser.oxpassword = " . $oDb->quote($this->encodePassword($sPassword, $sSalt)); + + $sSelect = "select `oxid` from oxuser where 1 and {$sPassSelect} and {$sUserSelect} "; + + return $sSelect; + } + + /** + * @return d3totp + * @throws DatabaseConnectionException + * @throws DBALException + */ + public function d3getTotp() + { + $oTotp = oxNew(d3totp::class); + $oTotp->loadByUserId($this->getId()); + + return $oTotp; + } } \ No newline at end of file diff --git a/src/metadata.php b/src/metadata.php index a1caa33..57d5c16 100644 --- a/src/metadata.php +++ b/src/metadata.php @@ -47,7 +47,7 @@ $aModule = [ 'email' => 'support@shopmodule.com', 'url' => 'http://www.oxidmodule.com/', 'extend' => [ - //OxidModel\User::class => \D3\Totp\Modules\Application\Model\d3_totp_user::class, + OxidModel\User::class => \D3\Totp\Modules\Application\Model\d3_totp_user::class, LoginController::class => \D3\Totp\Modules\Application\Controller\Admin\d3_totp_LoginController::class, Utils::class => \D3\Totp\Modules\Core\d3_totp_utils::class, ],