integrate backend, save encrypted seed only

This commit is contained in:
Daniel Seifert 2018-10-19 14:16:37 +02:00
parent fd3dbf5a12
commit 2f196aaef7
8 changed files with 274 additions and 87 deletions

View File

@ -16,12 +16,18 @@
namespace D3\Totp\Application\Controller\Admin; namespace D3\Totp\Application\Controller\Admin;
use D3\Totp\Application\Model\d3totp; use D3\Totp\Application\Model\d3totp;
use D3\Totp\Modules\Application\Model\d3_totp_user;
use Doctrine\DBAL\DBALException; use Doctrine\DBAL\DBALException;
use OxidEsales\Eshop\Application\Controller\Admin\AdminDetailsController; use OxidEsales\Eshop\Application\Controller\Admin\AdminDetailsController;
use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException; use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
use OxidEsales\Eshop\Core\Exception\StandardException;
use OxidEsales\Eshop\Core\Registry;
class d3user_totp extends AdminDetailsController class d3user_totp extends AdminDetailsController
{ {
protected $_sSaveError = null;
protected $_sThisTemplate = 'd3user_totp.tpl'; protected $_sThisTemplate = 'd3user_totp.tpl';
/** /**
@ -33,18 +39,62 @@ class d3user_totp extends AdminDetailsController
{ {
parent::render(); parent::render();
$soxId = $this->_aViewData["oxid"] = $this->getEditObjectId(); $soxId = $this->getEditObjectId();
if (isset($soxId) && $soxId != "-1") { if (isset($soxId) && $soxId != "-1") {
/** @var d3totp $oTotp */ /** @var d3_totp_user $oUser */
$oTotp = oxNew(d3totp::class); $oUser = oxNew(User::class);
$oTotp->loadByUserId($soxId); if ($oUser->load($soxId)) {
$this->_aViewData["edit"] = $oTotp; $this->addTplParam("oxid", $oUser->getId());
} else {
$this->addTplParam("oxid", '-1');
}
$this->addTplParam("edit", $oUser);
} }
if (!$this->_allowAdminEdit($soxId)) { if ($this->_sSaveError) {
$this->_aViewData['readonly'] = true; $this->addTplParam("sSaveError", $this->_sSaveError);
} }
return $this->_sThisTemplate; 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();
}
}
} }

View File

@ -81,7 +81,8 @@ class d3totp extends BaseModel
*/ */
public function UserUseTotp() 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() public function getSavedSecret()
{ {
$secret = $this->getFieldData('seed'); $seed_enc = $this->getFieldData('seed');
$sPwd = Registry::getSession()->getVariable('pwdTransmit'); $sPwd = Registry::getSession()->getVariable('pwdTransmit');
if ($secret) { if ($seed_enc && $sPwd) {
return $secret; $seed = $this->decrypt($seed_enc, $sPwd);
if ($seed) {
return $seed;
}
} }
return null; return null;
} }
/** /**
* @param $seed
* @return TOTP * @return TOTP
*/ */
public function getTotp() public function getTotp($seed = null)
{ {
if (false == $this->totp) { if (false == $this->totp) {
@ -113,10 +118,12 @@ class d3totp extends BaseModel
$this->getUser()->getFieldData('oxusername') $this->getUser()->getFieldData('oxusername')
? $this->getUser()->getFieldData('oxusername') ? $this->getUser()->getFieldData('oxusername')
: null, : null,
$this->getSavedSecret() $seed
? $seed
: $this->getSavedSecret()
); );
} else { // version 0.9 (>= PHP 7.1) } 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->totp->setLabel($this->getUser()->getFieldData('oxusername')
? $this->getUser()->getFieldData('oxusername') ? $this->getUser()->getFieldData('oxusername')
: null : null
@ -151,17 +158,31 @@ class d3totp extends BaseModel
*/ */
public function getSecret() 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 $totp
* @param $seed
* @return string * @return string
* @throws d3totp_wrongOtpException * @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) { if (false == $blVerify) {
$oException = oxNew(d3totp_wrongOtpException::class, 'unvalid TOTP'); $oException = oxNew(d3totp_wrongOtpException::class, 'unvalid TOTP');
throw $oException; throw $oException;
@ -169,4 +190,43 @@ class d3totp extends BaseModel
return $blVerify; 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;
}
} }

View File

@ -12,6 +12,8 @@
<input type="text" name="d3totp" id="d3totp" value="" size="49" autofocus autocomplete="off"><br> <input type="text" name="d3totp" id="d3totp" value="" size="49" autofocus autocomplete="off"><br>
[{oxmultilang ident="TOTP_INPUT_HELP"}] [{oxmultilang ident="TOTP_INPUT_HELP"}]
--Anmeldung abbrechen--
[{else}] [{else}]
[{$smarty.block.parent}] [{$smarty.block.parent}]
[{/if}] [{/if}]

View File

@ -25,9 +25,13 @@ $aLang = [
'd3mxuser_totp' => '2-Faktor-Authentisierung', 'd3mxuser_totp' => '2-Faktor-Authentisierung',
'D3_TOTP_ACTIVE' => 'Benutzer verwendet 2-Faktor-Authentisierung', 'D3_TOTP_REGISTERNEW' => 'neue Registrierung erstellen',
'D3_TOTP_ACTIVE_HELP' => 'Benutzer verwendet 2-Faktor-Authentisierung',
'D3_TOTP_QRCODE' => 'QR-Code', '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' => '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' => '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.',
]; ];

View File

@ -1,5 +1,7 @@
[{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}] [{include file="headitem.tpl" title="GENERAL_ADMIN_TITLE"|oxmultilangassign}]
[{assign var="totp" value=$edit->d3GetTotp()}]
[{if $readonly}] [{if $readonly}]
[{assign var="readonly" value="readonly disabled"}] [{assign var="readonly" value="readonly disabled"}]
[{else}] [{else}]
@ -17,80 +19,98 @@
<input type="hidden" name="cl" value="[{$oViewConf->getActiveClassName()}]"> <input type="hidden" name="cl" value="[{$oViewConf->getActiveClassName()}]">
<input type="hidden" name="fnc" value=""> <input type="hidden" name="fnc" value="">
<input type="hidden" name="oxid" value="[{$oxid}]"> <input type="hidden" name="oxid" value="[{$oxid}]">
<input type="hidden" name="editval[d3totp__oxid]" value="[{$totp->getId()}]">
<input type="hidden" name="editval[d3totp__oxuserid]" value="[{$oxid}]"> <input type="hidden" name="editval[d3totp__oxuserid]" value="[{$oxid}]">
<table cellspacing="0" cellpadding="0" border="0" style="width:98%;">
<tr>
<td valign="top" class="edittext" style="padding-top:10px;padding-left:10px; width: 50%;">
<table cellspacing="0" cellpadding="0" border="0">
[{block name="user_d3user_totp_form1"}]
<tr>
<td class="edittext" width="120">
[{oxmultilang ident="D3_TOTP_ACTIVE"}]
</td>
<td class="edittext">
<input type="hidden" name="editval[d3totp__usetoptp]" value="0">
<input class="edittext" type="checkbox" name="editval[d3totp__usetotp]" value='1' [{if $edit->getFieldData('usetotp') == 1}]checked[{/if}] [{$readonly}]>
[{oxinputhelp ident="D3_TOTP_ACTIVE_HELP"}]
</td>
</tr>
[{/block}] [{if $sSaveError}]
<table cellspacing="0" cellpadding="0" border="0" style="width:98%;">
<tr>
<td></td>
<td class="errorbox">[{oxmultilang ident=$sSaveError}]</td>
</tr>
</table>
[{/if}]
<tr> [{if $oxid && $oxid != '-1'}]
<td class="edittext" colspan="2"><br><br> <table cellspacing="0" cellpadding="0" border="0" style="width:98%;">
<input type="submit" class="edittext" id="oLockButton" name="saveArticle" value="[{oxmultilang ident="ARTICLE_MAIN_SAVE"}]" onClick="Javascript:document.myedit.fnc.value='save'" [{if !$edit->oxarticles__oxtitle->value && !$oxparentid}]disabled[{/if}] [{$readonly}]> <tr>
[{if $oxid!=-1 && !$readonly}] <td valign="top" class="edittext" style="padding-top:10px;padding-left:10px; width: 50%;">
<input type="submit" class="edittext" name="save" value="[{oxmultilang ident="ARTICLE_MAIN_ARTCOPY"}]" onClick="Javascript:document.myedit.fnc.value='copyArticle';" [{$readonly}]>&nbsp;&nbsp;&nbsp; <table cellspacing="0" cellpadding="0" border="0">
[{block name="user_d3user_totp_form1"}]
[{if false == $totp->getId()}]
<tr>
<td class="edittext" colspan="2">
<h4>[{oxmultilang ident="D3_TOTP_REGISTERNEW"}]</h4>
</td>
</tr>
<tr>
<td class="edittext">
[{oxmultilang ident="D3_TOTP_QRCODE"}]&nbsp;
</td>
<td class="edittext">
<img src="[{$totp->getQrCodeUri()}]">
[{oxinputhelp ident="D3_TOTP_QRCODE_HELP"}]
</td>
</tr>
<tr>
<td class="edittext">
<label for="secret">[{oxmultilang ident="D3_TOTP_SECRET"}]</label>
</td>
<td class="edittext">
<textarea rows="3" cols="50" id="secret" name="secret" class="editinput" readonly="readonly">[{$totp->getSecret()}]</textarea>
[{oxinputhelp ident="D3_TOTP_SECRET_HELP"}]
</td>
</tr>
<tr>
<td class="edittext">
<label for="pwd">[{oxmultilang ident="D3_TOTP_CURRPWD"}]</label>
</td>
<td class="edittext">
<input type="password" class="editinput" size="15" id="pwd" name="pwd" value="" [{$readonly}]>
[{oxinputhelp ident="D3_TOTP_CURRPWD_HELP"}]
</td>
</tr>
<tr>
<td class="edittext">
<label for="otp">[{oxmultilang ident="D3_TOTP_CURROTP"}]</label>
</td>
<td class="edittext">
<input type="text" class="editinput" size="6" maxlength="6" id="otp" name="otp" value="" [{$readonly}]>
[{oxinputhelp ident="D3_TOTP_CURROTP_HELP"}]
</td>
</tr>
[{else}]
<tr>
<td class="edittext">
</td>
<td class="edittext">
neuen Zugang anlegen, alle bisherigen Zugännge werden damit ungültig
</td>
</tr>
[{/if}] [{/if}]
</td>
</tr> [{/block}]
</table>
</td>
<!-- Anfang rechte Seite -->
<td valign="top" class="edittext" align="left" style="height:99%;padding-left:5px;padding-bottom:30px;padding-top:10px; width: 50%;">
<table cellspacing="0" cellpadding="0" border="0">
[{block name="user_d3user_totp_form2"}]
<tr> <tr>
<td class="edittext"> <td class="edittext" colspan="2"><br><br>
[{oxmultilang ident="D3_TOTP_QRCODE"}]&nbsp; <input type="submit" class="edittext" id="oLockButton" name="saveArticle" value="[{oxmultilang ident="ARTICLE_MAIN_SAVE"}]" onClick="document.myedit.fnc.value='save'" [{$readonly}]>
</td>
<td class="edittext">
<img src="[{$edit->getQrCodeUri()}]">
[{*
<input type="text" class="editinput" size="32" maxlength="[{$edit->oxarticles__oxtitle->fldmax_length}]" id="oLockTarget" name="editval[oxarticles__oxtitle]" value="[{$edit->oxarticles__oxtitle->value}]">
[{oxinputhelp ident="HELP_ARTICLE_MAIN_TITLE"}]
*}]
</td> </td>
</tr> </tr>
<tr> </table>
<td class="edittext"> </td>
[{oxmultilang ident="D3_TOTP_SECRET"}]&nbsp; <!-- Anfang rechte Seite -->
</td> <td valign="top" class="edittext" align="left" style="height:99%;padding-left:5px;padding-bottom:30px;padding-top:10px; width: 50%;">
<td class="edittext"> <table cellspacing="0" cellpadding="0" border="0">
[{$edit->getSecret()}] [{block name="user_d3user_totp_form2"}][{/block}]
[{* </table>
<input type="text" class="editinput" size="32" maxlength="[{$edit->oxarticles__oxartnum->fldmax_length}]" name="editval[oxarticles__oxartnum]" value="[{$edit->oxarticles__oxartnum->value}]" [{$readonly}]> </td>
[{oxinputhelp ident="HELP_ARTICLE_MAIN_ARTNUM"}] <!-- Ende rechte Seite -->
*}] </tr>
</td> </table>
</tr> [{/if}]
<tr>
<td class="edittext">
[{oxmultilang ident="D3_TOTP_CURROTP"}]&nbsp;
</td>
<td class="edittext">
<input type="text" class="editinput" size="6" maxlength="6" name="otp" value="">
[{oxinputhelp ident="D3_TOTP_CURROTP_HELP"}]
</td>
</tr>
[{/block}]
</table>
</td>
<!-- Ende rechte Seite -->
</tr>
</table>
</form> </form>
[{include file="bottomnaviitem.tpl"}] [{include file="bottomnaviitem.tpl"}]

View File

@ -72,6 +72,7 @@ class d3_totp_LoginController extends d3_totp_LoginController_parent
$return = parent::checklogin(); $return = parent::checklogin();
} elseif ($this->hasValidTotp($sTotp, $totp)) { } elseif ($this->hasValidTotp($sTotp, $totp)) {
Registry::getSession()->setVariable(d3totp::TOTP_SESSION_VARNAME, $sTotp); Registry::getSession()->setVariable(d3totp::TOTP_SESSION_VARNAME, $sTotp);
Registry::getSession()->deleteVariable('pwdTransmit');
$return = "admin_start"; $return = "admin_start";
} }
} catch (d3totp_wrongOtpException $oEx) { } catch (d3totp_wrongOtpException $oEx) {

View File

@ -16,6 +16,9 @@
namespace D3\Totp\Modules\Application\Model; namespace D3\Totp\Modules\Application\Model;
use D3\Totp\Application\Model\d3totp; 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; use OxidEsales\Eshop\Core\Registry;
class d3_totp_user extends d3_totp_user_parent class d3_totp_user extends d3_totp_user_parent
@ -29,4 +32,51 @@ class d3_totp_user extends d3_totp_user_parent
return $return; 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;
}
} }

View File

@ -47,7 +47,7 @@ $aModule = [
'email' => 'support@shopmodule.com', 'email' => 'support@shopmodule.com',
'url' => 'http://www.oxidmodule.com/', 'url' => 'http://www.oxidmodule.com/',
'extend' => [ '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, LoginController::class => \D3\Totp\Modules\Application\Controller\Admin\d3_totp_LoginController::class,
Utils::class => \D3\Totp\Modules\Core\d3_totp_utils::class, Utils::class => \D3\Totp\Modules\Core\d3_totp_utils::class,
], ],