snappymail/plugins/two-factor-auth/index.php

321 lines
9 KiB
PHP
Raw Normal View History

2021-04-13 17:42:06 +08:00
<?php
use \RainLoop\Exceptions\ClientException;
class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Two Factor Authentication',
VERSION = '2.1',
RELEASE = '2021-07-22',
REQUIRED = '2.5.4',
2021-04-14 16:11:09 +08:00
CATEGORY = 'Login',
2021-07-23 19:36:38 +08:00
DESCRIPTION = 'This plugin allows you to have TOTP 2FA';
2021-04-13 17:42:06 +08:00
public function Init() : void
{
$this->UseLangs(true);
2021-04-14 16:11:09 +08:00
// $this->addCss('style.less');
2021-07-23 19:36:38 +08:00
$this->addJs('js/TwoFactorAuthLogin.js');
2021-04-13 17:42:06 +08:00
$this->addJs('js/TwoFactorAuthSettings.js');
2021-04-14 21:24:57 +08:00
2021-04-14 21:33:37 +08:00
$this->addHook('login.success', 'DoLogin');
2021-04-13 17:42:06 +08:00
$this->addJsonHook('GetTwoFactorInfo', 'DoGetTwoFactorInfo');
$this->addJsonHook('CreateTwoFactorSecret', 'DoCreateTwoFactorSecret');
$this->addJsonHook('ShowTwoFactorSecret', 'DoShowTwoFactorSecret');
$this->addJsonHook('EnableTwoFactor', 'DoEnableTwoFactor');
2021-04-14 16:11:09 +08:00
$this->addJsonHook('VerifyTwoFactorCode', 'DoVerifyTwoFactorCode');
2021-04-13 17:42:06 +08:00
$this->addJsonHook('ClearTwoFactorInfo', 'DoClearTwoFactorInfo');
$this->addTemplate('templates/TwoFactorAuthSettings.html');
$this->addTemplate('templates/PopupsTwoFactorAuthTest.html');
}
2021-04-14 21:33:37 +08:00
public function DoLogin(\RainLoop\Model\Account $oAccount)
2021-04-14 21:24:57 +08:00
{
2021-04-14 21:33:37 +08:00
if ($this->TwoFactorAuthProvider($oAccount)) {
2021-04-14 21:24:57 +08:00
$aData = $this->getTwoFactorInfo($oAccount);
if ($aData && isset($aData['IsSet'], $aData['Enable']) && !empty($aData['Secret']) && $aData['IsSet'] && $aData['Enable']) {
2021-07-23 19:36:38 +08:00
$sCode = \trim($this->jsonParam('totp_code', ''));
if (empty($sCode)) {
2021-07-23 22:18:23 +08:00
$this->Logger()->Write("TFA: Code required for {$oAccount->ParentEmailHelper()}");
throw new ClientException(\RainLoop\Notifications::AuthError);
2021-07-23 21:58:54 +08:00
}
2021-04-14 21:24:57 +08:00
2021-07-23 21:58:54 +08:00
$bUseBackupCode = false;
if (6 < \strlen($sCode) && !empty($aData['BackupCodes'])) {
$aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes'])));
$bUseBackupCode = \in_array($sCode, $aBackupCodes);
if ($bUseBackupCode) {
$this->removeBackupCodeFromTwoFactorInfo($oAccount->ParentEmailHelper(), $sCode);
2021-07-23 19:36:38 +08:00
}
2021-07-23 21:58:54 +08:00
}
2021-04-14 21:24:57 +08:00
2021-07-23 21:58:54 +08:00
if (!$bUseBackupCode && !$this->TwoFactorAuthProvider($oAccount)->VerifyCode($aData['Secret'], $sCode)) {
$this->Manager()->Actions()->LoggerAuthHelper($oAccount);
2021-07-23 22:18:23 +08:00
$this->Logger()->Write("TFA: Code failed for {$oAccount->ParentEmailHelper()}");
throw new ClientException(\RainLoop\Notifications::AuthError);
2021-04-14 21:24:57 +08:00
}
2021-07-23 22:18:23 +08:00
$this->Logger()->Write("TFA: Code verified for {$oAccount->ParentEmailHelper()}");
2021-04-14 21:24:57 +08:00
}
}
}
2021-04-13 17:42:06 +08:00
public function DoGetTwoFactorInfo() : array
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true));
2021-04-13 17:42:06 +08:00
}
public function DoCreateTwoFactorSecret() : array
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
$sEmail = $oAccount->ParentEmailHelper();
2021-04-14 21:24:57 +08:00
$sSecret = $this->TwoFactorAuthProvider($oAccount)->CreateSecret();
2021-04-13 17:42:06 +08:00
$aCodes = array();
for ($iIndex = 9; $iIndex > 0; $iIndex--)
{
$aCodes[] = \rand(100000000, 900000000);
}
$this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => false,
'Secret' => $sSecret,
'BackupCodes' => \implode(' ', $aCodes)
))
);
2021-07-23 19:36:38 +08:00
// $this->Manager()->Actions()->requestSleep();
2021-04-13 17:42:06 +08:00
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount));
2021-04-13 17:42:06 +08:00
}
public function DoShowTwoFactorSecret() : array
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
$aResult = $this->getTwoFactorInfo($oAccount);
unset($aResult['BackupCodes']);
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, $aResult);
2021-04-13 17:42:06 +08:00
}
public function DoEnableTwoFactor() : array
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
2021-04-14 21:24:57 +08:00
// $this->Manager()->Actions()->setSettingsFromParams($oSettings, 'EnableTwoFactor', 'bool');
$oSettings = $this->Manager()->Actions()->SettingsProvider()->Load($oAccount);
if ($this->Manager()->Actions()->HasActionParam('EnableTwoFactor')) {
$sValue = $this->GetActionParam('EnableTwoFactor', '');
$oSettings->SetConf('EnableTwoFactor', !empty($sValue));
}
2021-04-13 17:42:06 +08:00
$sEmail = $oAccount->ParentEmailHelper();
$bResult = false;
$mData = $this->getTwoFactorInfo($oAccount);
if (isset($mData['Secret'], $mData['BackupCodes']))
{
$bResult = $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues(array(
'User' => $sEmail,
'Enable' => '1' === \trim($this->jsonParam('Enable', '0')),
'Secret' => $mData['Secret'],
'BackupCodes' => $mData['BackupCodes']
))
);
}
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, $bResult);
2021-04-13 17:42:06 +08:00
}
2021-04-14 16:11:09 +08:00
public function DoVerifyTwoFactorCode() : array
2021-04-13 17:42:06 +08:00
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
$sCode = \trim($this->jsonParam('Code', ''));
$aData = $this->getTwoFactorInfo($oAccount);
$sSecret = !empty($aData['Secret']) ? $aData['Secret'] : '';
// $this->Logger()->WriteDump(array(
// $sCode, $sSecret, $aData,
2021-04-14 21:24:57 +08:00
// $this->TwoFactorAuthProvider($oAccount)->VerifyCode($sSecret, $sCode)
2021-04-13 17:42:06 +08:00
// ));
2021-07-23 19:36:38 +08:00
// $this->Manager()->Actions()->requestSleep();
2021-04-13 17:42:06 +08:00
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__,
2021-04-14 21:24:57 +08:00
$this->TwoFactorAuthProvider($oAccount)->VerifyCode($sSecret, $sCode));
2021-04-13 17:42:06 +08:00
}
public function DoClearTwoFactorInfo() : array
{
$oAccount = $this->getAccountFromToken();
2021-04-14 21:24:57 +08:00
if (!$this->TwoFactorAuthProvider($oAccount)) {
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, false);
2021-04-13 17:42:06 +08:00
}
$this->StorageProvider()->Clear($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
2021-07-23 19:36:38 +08:00
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true));
2021-04-14 21:24:57 +08:00
}
2021-07-23 21:58:54 +08:00
protected function Logger() : \MailSo\Log\Logger
2021-04-14 21:24:57 +08:00
{
return $this->Manager()->Actions()->Logger();
}
protected function getAccountFromToken() : \RainLoop\Model\Account
{
return $this->Manager()->Actions()->GetAccount();
}
protected function StorageProvider() : \RainLoop\Providers\Storage
{
return $this->Manager()->Actions()->StorageProvider();
2021-04-13 17:42:06 +08:00
}
2021-04-14 21:24:57 +08:00
private $oTwoFactorAuthProvider;
protected function TwoFactorAuthProvider(\RainLoop\Model\Account $oAccount) : ?TwoFactorAuthInterface
2021-04-13 17:42:06 +08:00
{
2021-07-23 19:36:38 +08:00
if (!$this->oTwoFactorAuthProvider) {
2021-04-13 17:42:06 +08:00
require __DIR__ . '/providers/interface.php';
require __DIR__ . '/providers/totp.php';
$this->oTwoFactorAuthProvider = new TwoFactorAuthTotp();
}
return $this->oTwoFactorAuthProvider;
}
protected function getTwoFactorInfo(\RainLoop\Model\Account $oAccount, bool $bRemoveSecret = false) : array
{
$sEmail = $oAccount->ParentEmailHelper();
$mData = null;
$aResult = array(
'User' => '',
'IsSet' => false,
'Enable' => false,
'Secret' => '',
'UrlTitle' => '',
'BackupCodes' => ''
);
if (!empty($sEmail))
{
$aResult['User'] = $sEmail;
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
}
}
if (!empty($aResult['User']) &&
!empty($mData['User']) && !empty($mData['Secret']) &&
!empty($mData['BackupCodes']) && $sEmail === $mData['User'])
{
$aResult['IsSet'] = true;
$aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false;
$aResult['Secret'] = $mData['Secret'];
$aResult['BackupCodes'] = $mData['BackupCodes'];
$aResult['UrlTitle'] = $this->Config()->Get('webmail', 'title', '');
}
if ($bRemoveSecret)
{
if (isset($aResult['Secret']))
{
unset($aResult['Secret']);
}
if (isset($aResult['UrlTitle']))
{
unset($aResult['UrlTitle']);
}
if (isset($aResult['BackupCodes']))
{
unset($aResult['BackupCodes']);
}
}
return $aResult;
}
protected function removeBackupCodeFromTwoFactorInfo(\RainLoop\Model\Account $oAccount, string $sCode) : bool
{
if (!$oAccount || empty($sCode))
{
return false;
}
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = \RainLoop\Utils::DecodeKeyValues($sData);
if (!empty($mData['BackupCodes']))
{
$sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' ');
$sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes);
$mData['BackupCodes'] = \trim(\preg_replace('/[^\d]+/', ' ', $sBackupCodes));
return $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\RainLoop\Utils::EncodeKeyValues($mData)
);
}
}
return false;
}
}