oxtotp/src/Application/Model/d3totp.php

288 lines
7.3 KiB
PHP
Raw Normal View History

2018-10-20 23:39:22 +02:00
<?php
/**
2022-09-26 15:22:26 +02:00
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
2018-10-20 23:39:22 +02:00
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
2022-09-26 15:22:26 +02:00
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @link https://www.oxidmodule.com
2018-10-20 23:39:22 +02:00
*/
2022-09-28 00:08:36 +02:00
declare(strict_types=1);
2018-10-20 23:39:22 +02:00
namespace D3\Totp\Application\Model;
2019-08-12 23:57:44 +02:00
use BaconQrCode\Renderer\RendererInterface;
use BaconQrCode\Writer;
use D3\Totp\Application\Factory\BaconQrCodeFactory;
2018-10-20 23:39:22 +02:00
use D3\Totp\Application\Model\Exceptions\d3totp_wrongOtpException;
use OTPHP\TOTP;
use OxidEsales\Eshop\Application\Model\User;
2019-08-12 23:57:44 +02:00
use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface;
2018-10-20 23:39:22 +02:00
use OxidEsales\Eshop\Core\DatabaseProvider;
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
use OxidEsales\Eshop\Core\Model\BaseModel;
use OxidEsales\Eshop\Core\Registry;
class d3totp extends BaseModel
{
2022-09-30 21:06:30 +02:00
public const TOTP_SESSION_VARNAME = 'totp_auth';
public const TOTP_SESSION_CURRENTUSER = 'd3totpCurrentUser';
public const TOTP_SESSION_CURRENTCLASS = 'd3totpCurrentClass';
public const TOTP_SESSION_NAVFORMPARAMS = 'd3totpNavFormParams';
2018-10-20 23:39:22 +02:00
public $tableName = 'd3totp';
public $userId;
public $totp;
2018-10-23 22:35:44 +02:00
protected $timeWindow = 2;
2018-10-20 23:39:22 +02:00
/**
* d3totp constructor.
*/
public function __construct()
{
$this->init($this->tableName);
2022-10-01 14:44:36 +02:00
parent::__construct();
2018-10-20 23:39:22 +02:00
}
/**
* @param $userId
* @throws DatabaseConnectionException
*/
public function loadByUserId($userId)
{
$this->userId = $userId;
2019-08-12 23:57:44 +02:00
$oDb = $this->d3GetDb();
2018-10-20 23:39:22 +02:00
2019-08-12 23:57:44 +02:00
if ($oDb->getOne("SHOW TABLES LIKE '".$this->tableName."'")) {
$query = "SELECT oxid FROM ".$this->getViewName().' WHERE oxuserid = '.$oDb->quote($userId).' LIMIT 1';
$this->load($oDb->getOne($query));
2018-10-20 23:39:22 +02:00
}
}
/**
* @param $userId
* @return bool
* @throws DatabaseConnectionException
*/
public function checkIfAlreadyExist($userId)
{
$oDb = $this->d3GetDb();
$query = "SELECT 1 FROM ".$this->getViewName().' WHERE oxuserid = '.$oDb->quote($userId).' LIMIT 1';
return (bool) $oDb->getOne($query);
}
2019-08-12 23:57:44 +02:00
/**
* @return DatabaseInterface
* @throws DatabaseConnectionException
*/
public function d3GetDb()
{
return DatabaseProvider::getDb(DatabaseProvider::FETCH_MODE_ASSOC);
}
2018-10-20 23:39:22 +02:00
/**
* @return User
*/
public function getUser()
{
2022-09-26 15:58:38 +02:00
$userId = $this->userId ?: $this->getFieldData('oxuserid');
2018-10-20 23:39:22 +02:00
2019-08-12 23:57:44 +02:00
$user = $this->d3GetUser();
2018-10-20 23:39:22 +02:00
$user->load($userId);
return $user;
}
2019-08-12 23:57:44 +02:00
/**
* @return User
*/
public function d3GetUser()
{
return oxNew(User::class);
}
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');
}
/**
2022-10-01 14:44:36 +02:00
* @return string|null
2018-10-20 23:39:22 +02:00
*/
2022-10-01 14:44:36 +02:00
public function getSavedSecret(): ?string
2018-10-20 23:39:22 +02:00
{
$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;
}
/**
* @param $seed
* @return TOTP
*/
public function getTotp($seed = null)
{
if (false == $this->totp) {
2022-09-26 16:07:47 +02:00
$this->totp = TOTP::create($seed ?: $this->getSavedSecret());
$this->totp->setLabel($this->getUser()->getFieldData('oxusername'));
2018-10-20 23:39:22 +02:00
$this->totp->setIssuer(Registry::getConfig()->getActiveShop()->getFieldData('oxname'));
}
return $this->totp;
}
/**
* @return string
*/
public function getQrCodeElement()
{
$renderer = BaconQrCodeFactory::renderer(200);
2019-08-12 23:57:44 +02:00
$writer = $this->d3GetWriter($renderer);
return $writer->writeString($this->getTotp()->getProvisioningUri());
}
2019-08-12 23:57:44 +02:00
/**
* @param RendererInterface $renderer
* @return Writer
*/
public function d3GetWriter(RendererInterface $renderer)
{
return oxNew(Writer::class, $renderer);
}
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(
[
2022-09-30 21:06:30 +02:00
'seed' => $this->encrypt($seed),
]
2018-10-20 23:39:22 +02:00
);
}
/**
* @param $totp
* @param $seed
2022-10-01 14:44:36 +02:00
* @return bool
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)
{
2022-09-28 00:08:36 +02:00
$blNotVerified = $this->getTotp($seed)->verify($totp, null, $this->timeWindow) == false;
2022-09-28 00:08:36 +02:00
if ($blNotVerified && null == $seed) {
2019-08-12 23:57:44 +02:00
$oBC = $this->d3GetBackupCodeListObject();
2022-09-28 00:08:36 +02:00
$blNotVerified = $oBC->verify($totp) == false;
2019-07-28 00:07:16 +02:00
2022-09-28 00:08:36 +02:00
if ($blNotVerified) {
throw oxNew(d3totp_wrongOtpException::class);
2019-07-28 00:07:16 +02:00
}
2022-10-01 14:44:36 +02:00
} elseif ($blNotVerified && $seed !== null) {
2022-09-28 00:08:36 +02:00
throw oxNew(d3totp_wrongOtpException::class);
2018-10-20 23:39:22 +02:00
}
2022-09-28 00:08:36 +02:00
return !$blNotVerified;
2018-10-20 23:39:22 +02:00
}
2019-08-12 23:57:44 +02:00
/**
* @return d3backupcodelist
*/
public function d3GetBackupCodeListObject()
{
return oxNew(d3backupcodelist::class);
}
2018-10-20 23:39:22 +02:00
/**
* @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);
2022-09-30 00:06:20 +02:00
$ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, OPENSSL_RAW_DATA, $iv);
$hmac = hash_hmac('sha256', $ciphertext_raw, $key, true);
2019-08-13 23:09:51 +02:00
return base64_encode($iv.$hmac.$ciphertext_raw);
2018-10-20 23:39:22 +02:00
}
/**
* @param $ciphertext
* @return bool|string
*/
public function decrypt($ciphertext)
2018-10-20 23:39:22 +02:00
{
$key = Registry::getConfig()->getConfigParam('sConfigKey');
2019-08-12 23:57:44 +02:00
$c = $this->d3Base64_decode($ciphertext);
2018-10-20 23:39:22 +02:00
$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);
2022-09-30 00:06:20 +02:00
$original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $key, OPENSSL_RAW_DATA, $iv);
$calcmac = hash_hmac('sha256', $ciphertext_raw, $key, true);
2018-10-20 23:39:22 +02:00
if (hash_equals($hmac, $calcmac)) { // PHP 5.6+ compute attack-safe comparison
return $original_plaintext;
}
return false;
}
2019-08-12 23:57:44 +02:00
/**
* required for unit tests
* @param $source
* @return bool|string
*/
public function d3Base64_decode($source)
{
return base64_decode($source);
}
/**
* @param null $oxid
* @return bool
* @throws DatabaseConnectionException
*/
public function delete($oxid = null)
{
2019-08-12 23:57:44 +02:00
$oBackupCodeList = $this->d3GetBackupCodeListObject();
$oBackupCodeList->deleteAllFromUser($this->getFieldData('oxuserid'));
2022-10-01 14:44:36 +02:00
return parent::delete($oxid);
}
}