oxtotp/src/Application/Model/d3totp.php

286 lines
7.8 KiB
PHP
Raw Normal View History

2018-10-20 23:39:22 +02:00
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @link http://www.oxidmodule.com
*/
namespace D3\Totp\Application\Model;
use BaconQrCode\Renderer\Image\Svg;
use BaconQrCode\Writer;
2018-10-20 23:39:22 +02:00
use D3\ModCfg\Application\Model\d3database;
use D3\Totp\Application\Model\Exceptions\d3totp_wrongOtpException;
use Doctrine\DBAL\DBALException;
use OTPHP\TOTP;
use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\DatabaseProvider;
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
use OxidEsales\Eshop\Core\Model\BaseModel;
use OxidEsales\Eshop\Core\Registry;
2019-07-27 00:03:22 +02:00
use RandomLib\Factory;
use RandomLib\Generator;
2018-10-20 23:39:22 +02:00
class d3totp extends BaseModel
{
const TOTP_SESSION_VARNAME = 'totp_auth';
public $tableName = 'd3totp';
public $userId;
public $totp;
2018-10-23 22:35:44 +02:00
protected $timeWindow = 2;
2019-07-27 00:03:22 +02:00
protected $_backupCodes = array();
2018-10-20 23:39:22 +02:00
/**
* d3totp constructor.
*/
public function __construct()
{
$this->init($this->tableName);
return parent::__construct();
}
/**
* @param $userId
* @return bool
* @throws DBALException
* @throws DatabaseConnectionException
*/
public function loadByUserId($userId)
{
$this->userId = $userId;
$oQB = d3database::getInstance()->getQueryBuilder();
if (DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->getOne("SHOW TABLES LIKE '".$this->tableName."'")) {
$oQB->select('oxid')
->from($this->getViewName())
->where("oxuserid = " . $oQB->createNamedParameter($userId))
->setMaxResults(1);
return $this->load(DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC)->getOne($oQB->getSQL(), $oQB->getParameters()));
}
return false;
}
/**
* @return User
*/
public function getUser()
{
$userId = $this->userId ? $this->userId : $this->getFieldData('oxuserid');
$user = oxNew(User::class);
$user->load($userId);
return $user;
}
2018-10-21 20:54:58 +02:00
/**
* @return bool
*/
public function isActive()
{
return false == Registry::getConfig()->getConfigParam('blDisableTotpGlobally')
&& $this->UserUseTotp();
}
2018-10-20 23:39:22 +02:00
/**
* @return bool
*/
public function UserUseTotp()
{
return $this->getFieldData('usetotp')
&& $this->getFieldData('seed');
}
/**
* @return string
*/
public function getSavedSecret()
{
$seed_enc = $this->getFieldData('seed');
if ($seed_enc) {
$seed = $this->decrypt($seed_enc);
2018-10-20 23:39:22 +02:00
if ($seed) {
return $seed;
}
}
return null;
}
2019-07-27 00:03:22 +02:00
/**
* @return array
2019-07-28 00:07:16 +02:00
* @throws DatabaseConnectionException
2019-07-27 00:03:22 +02:00
*/
public function generateBackupCodes()
{
$factory = new Factory();
$generator = $factory->getLowStrengthGenerator();
2019-07-28 00:07:16 +02:00
for ($i = 1; $i <= 10; $i++) {
$sCode = $generator->generateString(6, Generator::CHAR_DIGITS);
$this->_backupCodes[] = $sCode;
$this->assign(
array(
'bc'.$i => $this->d3EncodeBC($sCode)
)
);
2019-07-27 00:03:22 +02:00
}
return $this->_backupCodes;
}
2018-10-20 23:39:22 +02:00
/**
* @param $seed
* @return TOTP
*/
public function getTotp($seed = null)
{
if (false == $this->totp) {
$this->totp = oxNew(
TOTP::class,
$this->getUser()->getFieldData('oxusername')
2018-10-20 23:39:22 +02:00
? $this->getUser()->getFieldData('oxusername')
: null,
$seed
? $seed
: $this->getSavedSecret()
);
2018-10-20 23:39:22 +02:00
$this->totp->setIssuer(Registry::getConfig()->getActiveShop()->getFieldData('oxname'));
}
return $this->totp;
}
/**
* @return string
*/
public function getQrCodeUri()
{
return $this->getTotp()->getQrCodeUri();
}
/**
* @return string
*/
public function getQrCodeElement()
{
$renderer = oxNew(Svg::class);
$renderer->setHeight(200);
$renderer->setWidth(200);
/** @var Writer $writer */
$writer = oxNew(Writer::class, $renderer);
return $writer->writeString($this->getTotp()->getProvisioningUri());
}
2018-10-20 23:39:22 +02:00
/**
* @return string
*/
public function getSecret()
{
return trim($this->getTotp()->getSecret());
}
/**
* @param $seed
*/
public function saveSecret($seed)
2018-10-20 23:39:22 +02:00
{
$this->assign(
array(
'seed' => $this->encrypt($seed)
2018-10-20 23:39:22 +02:00
)
);
}
2019-07-27 00:03:22 +02:00
/**
* @param $code
* @return false|string
* @throws DatabaseConnectionException
*/
public function d3EncodeBC($code)
{
$oDb = DatabaseProvider::getDb();
$salt = $this->getUser()->getFieldData('oxpasssalt');
$sSelect = "SELECT BINARY MD5( CONCAT( " . $oDb->quote($code) . ", UNHEX( ".$oDb->quote($salt)." ) ) )";
return $oDb->getOne($sSelect);
}
2018-10-20 23:39:22 +02:00
/**
* @param $totp
* @param $seed
* @return string
2019-07-28 00:07:16 +02:00
* @throws DatabaseConnectionException
2018-10-20 23:39:22 +02:00
* @throws d3totp_wrongOtpException
*/
public function verify($totp, $seed = null)
{
2018-10-23 22:35:44 +02:00
$blVerify = $this->getTotp($seed)->verify($totp, null, $this->timeWindow);
2018-10-20 23:39:22 +02:00
if (false == $blVerify) {
2019-07-28 00:07:16 +02:00
$oDb = DatabaseProvider::getDb();
$aFields = array('bc1', 'bc2', 'bc3', 'bc4', 'bc5', 'bc6', 'bc7', 'bc8', 'bc9', 'bc10');
$query = "SELECT 1 FROM ".$this->getViewName().
" WHERE ".$oDb->quote($this->d3EncodeBC($totp))." IN (".implode(', ', array_map([$oDb, 'quoteIdentifier'], $aFields)).") AND ".
$oDb->quoteIdentifier("oxuserid") ." = ".$oDb->quote($this->getUser()->getId());
$blVerify = (bool) $oDb->getOne($query);
if (false == $blVerify) {
$oException = oxNew(d3totp_wrongOtpException::class);
throw $oException;
}
2018-10-20 23:39:22 +02:00
}
return $blVerify;
}
/**
* @param $plaintext
* @return string
*/
public function encrypt($plaintext)
2018-10-20 23:39:22 +02:00
{
$key = Registry::getConfig()->getConfigParam('sConfigKey');
2018-10-20 23:39:22 +02:00
$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);
}
/**
* @param $ciphertext
* @return bool|string
*/
public function decrypt($ciphertext)
2018-10-20 23:39:22 +02:00
{
$key = Registry::getConfig()->getConfigParam('sConfigKey');
2018-10-20 23:39:22 +02:00
$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;
}
2018-10-18 15:33:59 +02:00
}