From f92810069aa94de98608893e6e6fa661b25c68bd Mon Sep 17 00:00:00 2001 From: djmaze Date: Mon, 8 Nov 2021 10:17:57 +0100 Subject: [PATCH] Improved Auth functions as trait and use JSON instead of serialize for encoding/decoding data --- .../0.0.0/app/libraries/RainLoop/Actions.php | 347 +--------------- .../libraries/RainLoop/Actions/Response.php | 2 +- .../app/libraries/RainLoop/Actions/User.php | 11 - .../libraries/RainLoop/Actions/UserAuth.php | 371 ++++++++++++++++++ .../v/0.0.0/app/libraries/RainLoop/Api.php | 2 +- .../app/libraries/RainLoop/Model/Account.php | 2 +- .../v/0.0.0/app/libraries/RainLoop/Utils.php | 57 +-- 7 files changed, 411 insertions(+), 381 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php index f3c6ab1b8..27d0a3cda 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -10,13 +10,14 @@ class Actions { use Actions\Admin; use Actions\User; + use Actions\UserAuth; use Actions\Raw; use Actions\Response; use Actions\Localization; use Actions\Themes; - const AUTH_SIGN_ME_TOKEN_KEY = 'rlsmauth'; const AUTH_MAILTO_TOKEN_KEY = 'rlmailtoauth'; + const AUTH_SIGN_ME_TOKEN_KEY = 'rlsmauth'; const AUTH_SPEC_TOKEN_KEY = 'rlspecauth'; const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'rlspeclogout'; const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'rlspeclogoutcmk'; @@ -106,11 +107,6 @@ class Actions */ private $oConfig; - /** - * @var string - */ - private $sSpecAuthToken; - /** * @access private */ @@ -143,13 +139,6 @@ class Actions $this->Logger()->Ping(); } - public function SetSpecAuthToken(string $sSpecAuthToken): self - { - $this->sSpecAuthToken = $sSpecAuthToken; - - return $this; - } - public function SetIsJson(bool $bIsJson): self { $this->bIsJson = $bIsJson; @@ -157,27 +146,11 @@ class Actions return $this; } - public function GetSpecAuthToken(): string - { - return $this->sSpecAuthToken; - } - public function GetIsJson(): bool { return $this->bIsJson; } - public function GetShortLifeSpecAuthToken(int $iLife = 60): string - { - $aAccountHash = Utils::DecodeKeyValues($this->getLocalAuthToken()); - if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0]) { - $aAccountHash[10] = \time() + $iLife; - return Utils::EncodeKeyValues($aAccountHash); - } - - return ''; - } - public function Config(): Config\Application { if (null === $this->oConfig) { @@ -321,30 +294,6 @@ class Actions return $sQuery; } - // rlspecauth / AuthAccountHash - public function getAuthAccountHash() : string - { - if ('' === $this->sSpecAuthToken && !\strlen($this->GetSpecAuthLogoutTokenWithDeletion())) { - $sAuthAccountHash = $this->GetSpecAuthTokenCookie() ?: $this->GetSpecAuthToken(); - if (empty($sAuthAccountHash)) { - $oAccount = $this->GetAccountFromSignMeToken(); - if ($oAccount) try - { - $this->CheckMailConnection($oAccount); - $this->AuthToken($oAccount); - $sAuthAccountHash = $this->GetSpecAuthToken(); - } - catch (\Throwable $oException) - { - $oException = null; - $this->ClearSignMeData($oAccount); - } - } - $this->SetSpecAuthToken($sAuthAccountHash); - } - return $this->GetSpecAuthToken(); - } - private function compileLogParams(string $sLine, ?Model\Account $oAccount = null, bool $bUrlEncode = false, array $aAdditionalParams = array()): string { $aClear = array(); @@ -472,75 +421,6 @@ class Actions return $sFileName; } - public function SetAuthLogoutToken(): void - { - \header('X-RainLoop-Action: Logout'); - Utils::SetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, \md5($_SERVER['REQUEST_TIME_FLOAT']), 0); - } - - public function SetAuthToken(Model\Account $oAccount): void - { - $sSpecAuthToken = '_' . $oAccount->GetAuthTokenQ(); - - $this->SetSpecAuthToken($sSpecAuthToken); - Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken); - - if ($oAccount->SignMe() && \strlen($oAccount->SignMeToken())) { - Utils::SetCookie(self::AUTH_SIGN_ME_TOKEN_KEY, - Utils::EncodeKeyValuesQ(array( - 'e' => $oAccount->Email(), - 't' => $oAccount->SignMeToken() - )), - \time() + 60 * 60 * 24 * 30); - - $this->StorageProvider()->Put($oAccount, - Providers\Storage\Enumerations\StorageType::CONFIG, - 'sign_me', - Utils::EncodeKeyValuesQ(array( - 'Time' => \time(), - 'AuthToken' => $oAccount->GetAuthTokenQ(), - 'SignMetToken' => $oAccount->SignMeToken() - )) - ); - } - } - - public function GetSpecAuthTokenCookie(): string - { - return Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY, ''); - } - - public function GetSpecAuthLogoutTokenWithDeletion(): string - { - $sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, ''); - if (0 < strlen($sResult)) { - Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY); - } - - return $sResult; - } - - public function GetSpecLogoutCustomMgsWithDeletion(): string - { - $sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, ''); - if (0 < strlen($sResult)) { - Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY); - } - - return $sResult; - } - - public function SetSpecLogoutCustomMgsWithDeletion(string $sMessage): string - { - Utils::SetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, $sMessage, 0); - } - - private function getLocalAuthToken(): string - { - $sToken = $this->GetSpecAuthToken(); - return !empty($sToken) && '_' === \substr($sToken, 0, 1) ? \substr($sToken, 1) : ''; - } - /** * @throws \RainLoop\Exceptions\ClientException */ @@ -858,109 +738,6 @@ class Actions } } - protected function LoginProvide(string $sEmail, string $sLogin, string $sPassword, string $sSignMeToken = '', string $sClientCert = '', bool $bThrowProvideException = false): ?Model\Account - { - $oAccount = null; - if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) { - $oDomain = $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true); - if ($oDomain) { - if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) { - $oAccount = new Model\Account($sEmail, $sLogin, $sPassword, $oDomain, $sSignMeToken, '', '', $sClientCert); - $this->Plugins()->RunHook('filter.account', array($oAccount)); - - if ($bThrowProvideException && !$oAccount) { - throw new Exceptions\ClientException(Notifications::AuthError); - } - } else if ($bThrowProvideException) { - throw new Exceptions\ClientException(Notifications::AccountNotAllowed); - } - } else if ($bThrowProvideException) { - throw new Exceptions\ClientException(Notifications::DomainNotAllowed); - } - } - - return $oAccount; - } - - /** - * @throws \RainLoop\Exceptions\ClientException - */ - public function GetAccountFromCustomToken(string $sToken, bool $bThrowExceptionOnFalse = true, bool $bValidateShortToken = true, bool $bQ = false): ?Model\Account - { - $oResult = null; - if (!empty($sToken)) { - $aAccountHash = $bQ ? Utils::DecodeKeyValuesQ($sToken) : Utils::DecodeKeyValues($sToken); - if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0] && // simple token validation - 8 <= \count($aAccountHash) && // length checking - !empty($aAccountHash[7]) && // does short token exist - (!$bValidateShortToken || Utils::GetShortToken() === $aAccountHash[7] || // check short token if needed - (isset($aAccountHash[10]) && 0 < $aAccountHash[10] && \time() < $aAccountHash[10])) - ) { - $oAccount = $this->LoginProvide($aAccountHash[1], $aAccountHash[2], $aAccountHash[3], - empty($aAccountHash[5]) ? '' : $aAccountHash[5], empty($aAccountHash[11]) ? '' : $aAccountHash[11], $bThrowExceptionOnFalse); - - if ($oAccount) { - // init proxy user/password - if (!empty($aAccountHash[8]) && !empty($aAccountHash[9])) { - $oAccount->SetProxyAuthUser($aAccountHash[8]); - $oAccount->SetProxyAuthPassword($aAccountHash[9]); - } - - $this->Logger()->AddSecret($oAccount->Password()); - $this->Logger()->AddSecret($oAccount->ProxyAuthPassword()); - - $oAccount->SetParentEmail($aAccountHash[6]); - $oResult = $oAccount; - } - } else if ($bThrowExceptionOnFalse) { - throw new Exceptions\ClientException(Notifications::AuthError); - } - } - - if ($bThrowExceptionOnFalse && !$oResult) { - throw new Exceptions\ClientException(Notifications::AuthError); - } - - return $oResult; - } - - public function GetAccountFromSignMeToken(): ?Model\Account - { - $oAccount = null; - - $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY, ''); - if (!empty($sSignMeToken)) { - $aTokenData = Utils::DecodeKeyValuesQ($sSignMeToken); - if (!empty($aTokenData['e']) && !empty($aTokenData['t'])) { - $sTokenSettings = $this->StorageProvider()->Get($aTokenData['e'], - Providers\Storage\Enumerations\StorageType::CONFIG, - 'sign_me' - ); - - if (!empty($sTokenSettings)) { - $aSignMeData = Utils::DecodeKeyValuesQ($sTokenSettings); - if (!empty($aSignMeData['AuthToken']) && - !empty($aSignMeData['SignMetToken']) && - $aSignMeData['SignMetToken'] === $aTokenData['t']) { - $oAccount = $this->GetAccountFromCustomToken($aSignMeData['AuthToken'], false, false, true); - } - } - } - } else { - Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); - } - - return $oAccount; - } - - /** - * @throws \RainLoop\Exceptions\ClientException - */ - public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Model\Account - { - return $this->GetAccountFromCustomToken($this->getLocalAuthToken(), $bThrowExceptionOnFalse, true, true); - } - public function AppDataSystem(bool $bAdmin = false): array { $oConfig = $this->Config(); @@ -1341,124 +1118,6 @@ class Actions ); } - /** - * @throws \RainLoop\Exceptions\ClientException - */ - public function LoginProcess(string &$sEmail, string &$sPassword, string $sSignMeToken = ''): Model\Account - { - $sInputEmail = $sEmail; - - $this->Plugins()->RunHook('login.credentials.step-1', array(&$sEmail)); - - $sEmail = \MailSo\Base\Utils::Trim($sEmail); - if ($this->Config()->Get('login', 'login_lowercase', true)) { - $sEmail = \MailSo\Base\Utils::StrToLowerIfAscii($sEmail); - } - - if (false === \strpos($sEmail, '@')) { - $this->Logger()->Write('The email address "' . $sEmail . '" is not complete', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - - if (false === \strpos($sEmail, '@') && !!$this->Config()->Get('login', 'determine_user_domain', false)) { - $sUserHost = \trim($this->Http()->GetHost(false, true, true)); - $this->Logger()->Write('Determined user domain: ' . $sUserHost, \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - - $bAdded = false; - - $iLimit = 14; - $aDomainParts = \explode('.', $sUserHost); - - $oDomainProvider = $this->DomainProvider(); - while (\count($aDomainParts) && 0 < $iLimit) { - $sLine = \trim(\implode('.', $aDomainParts), '. '); - - $oDomain = $oDomainProvider->Load($sLine, false); - if ($oDomain) { - $bAdded = true; - $this->Logger()->Write('Check "' . $sLine . '": OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', - \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - - $sEmail = $sEmail . '@' . $sLine; - break; - } else { - $this->Logger()->Write('Check "' . $sLine . '": NO', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - } - - \array_shift($aDomainParts); - $iLimit--; - } - - if (!$bAdded) { - $sLine = $sUserHost; - $oDomain = $oDomainProvider->Load($sLine, true); - if ($oDomain && $oDomain) { - $bAdded = true; - $this->Logger()->Write('Check "' . $sLine . '" with wildcard: OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', - \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - - $sEmail = $sEmail . '@' . $sLine; - } else { - $this->Logger()->Write('Check "' . $sLine . '" with wildcard: NO', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - } - } - - if (!$bAdded) { - $this->Logger()->Write('Domain was not found!', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - } - } - - $sDefDomain = \trim($this->Config()->Get('login', 'default_domain', '')); - if (false === \strpos($sEmail, '@') && \strlen($sDefDomain)) { - $this->Logger()->Write('Default domain "' . $sDefDomain . '" was used. (' . $sEmail . ' > ' . $sEmail . '@' . $sDefDomain . ')', - \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); - - $sEmail = $sEmail . '@' . $sDefDomain; - } - } - - $this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword)); - - if (false === \strpos($sEmail, '@') || !\strlen($sPassword)) { - $this->loginErrorDelay(); - - throw new Exceptions\ClientException(Notifications::InvalidInputArgument); - } - - $this->Logger()->AddSecret($sPassword); - - $sLogin = $sEmail; - if ($this->Config()->Get('login', 'login_lowercase', true)) { - $sLogin = \MailSo\Base\Utils::StrToLowerIfAscii($sLogin); - } - - $this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword)); - - $this->Logger()->AddSecret($sPassword); - - $oAccount = null; - $sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', '')); - try { - $oAccount = $this->LoginProvide($sEmail, $sLogin, $sPassword, $sSignMeToken, $sClientCert, true); - - if (!$oAccount) { - throw new Exceptions\ClientException(Notifications::AuthError); - } - } catch (\Throwable $oException) { - $this->loginErrorDelay(); - $this->LoggerAuthHelper($oAccount, $this->getAdditionalLogParamsByUserLogin($sInputEmail)); - throw $oException; - } - - try { - $this->CheckMailConnection($oAccount, true); - } catch (\Throwable $oException) { - $this->loginErrorDelay(); - - throw $oException; - } - - return $oAccount; - } - public function GetAccounts(Model\Account $oAccount): array { if ($this->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { @@ -1871,7 +1530,7 @@ class Actions $this->Config()->Get('labs', 'allow_message_append', false) && isset($_FILES, $_FILES['AppendFile'], $_FILES['AppendFile']['name'], $_FILES['AppendFile']['tmp_name'], $_FILES['AppendFile']['size'])) { - if (is_string($_FILES['AppendFile']['tmp_name']) && 0 < strlen($_FILES['AppendFile']['tmp_name'])) { + if (is_string($_FILES['AppendFile']['tmp_name']) && \strlen($_FILES['AppendFile']['tmp_name'])) { if (\UPLOAD_ERR_OK === (int)$_FILES['AppendFile']['error'] && !empty($sFolderFullNameRaw)) { $sSavedName = 'append-post-' . md5($sFolderFullNameRaw . $_FILES['AppendFile']['name'] . $_FILES['AppendFile']['tmp_name']); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php index 10f593d04..5c4547f1b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Response.php @@ -289,7 +289,7 @@ trait Response $mResult['References'] = $mResponse->References(); $fAdditionalExternalFilter = null; - if (!!$this->Config()->Get('labs', 'use_local_proxy_for_external_images', false)) + if ($this->Config()->Get('labs', 'use_local_proxy_for_external_images', false)) { $fAdditionalExternalFilter = function ($sUrl) { return './?/ProxyExternal/'.Utils::EncodeKeyValuesQ(array( diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php index 8453949c0..b10bb575b 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/User.php @@ -515,17 +515,6 @@ trait User $this->SettingsProvider()->Save($oAccount, $oSettings) : false); } - protected function ClearSignMeData(\RainLoop\Model\Account $oAccount) : void - { - if ($oAccount) { - Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); - $this->StorageProvider()->Clear($oAccount, - \RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG, - 'sign_me' - ); - } - } - private function generateSignMeToken(string $sEmail) : string { return \MailSo\Base\Utils::Sha1Rand(APP_SALT.$sEmail); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php new file mode 100644 index 000000000..c826dfffa --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -0,0 +1,371 @@ +Plugins()->RunHook('login.credentials.step-1', array(&$sEmail)); + + $sEmail = \MailSo\Base\Utils::Trim($sEmail); + if ($this->Config()->Get('login', 'login_lowercase', true)) { + $sEmail = \MailSo\Base\Utils::StrToLowerIfAscii($sEmail); + } + + if (false === \strpos($sEmail, '@')) { + $this->Logger()->Write('The email address "' . $sEmail . '" is not complete', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + + if (false === \strpos($sEmail, '@') && !!$this->Config()->Get('login', 'determine_user_domain', false)) { + $sUserHost = \trim($this->Http()->GetHost(false, true, true)); + $this->Logger()->Write('Determined user domain: ' . $sUserHost, \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + + $bAdded = false; + + $iLimit = 14; + $aDomainParts = \explode('.', $sUserHost); + + $oDomainProvider = $this->DomainProvider(); + while (\count($aDomainParts) && 0 < $iLimit) { + $sLine = \trim(\implode('.', $aDomainParts), '. '); + + $oDomain = $oDomainProvider->Load($sLine, false); + if ($oDomain) { + $bAdded = true; + $this->Logger()->Write('Check "' . $sLine . '": OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', + \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + + $sEmail = $sEmail . '@' . $sLine; + break; + } else { + $this->Logger()->Write('Check "' . $sLine . '": NO', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + } + + \array_shift($aDomainParts); + $iLimit--; + } + + if (!$bAdded) { + $sLine = $sUserHost; + $oDomain = $oDomainProvider->Load($sLine, true); + if ($oDomain && $oDomain) { + $bAdded = true; + $this->Logger()->Write('Check "' . $sLine . '" with wildcard: OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')', + \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + + $sEmail = $sEmail . '@' . $sLine; + } else { + $this->Logger()->Write('Check "' . $sLine . '" with wildcard: NO', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + } + } + + if (!$bAdded) { + $this->Logger()->Write('Domain was not found!', \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + } + } + + $sDefDomain = \trim($this->Config()->Get('login', 'default_domain', '')); + if (false === \strpos($sEmail, '@') && \strlen($sDefDomain)) { + $this->Logger()->Write('Default domain "' . $sDefDomain . '" was used. (' . $sEmail . ' > ' . $sEmail . '@' . $sDefDomain . ')', + \MailSo\Log\Enumerations\Type::INFO, 'LOGIN'); + + $sEmail = $sEmail . '@' . $sDefDomain; + } + } + + $this->Plugins()->RunHook('login.credentials.step-2', array(&$sEmail, &$sPassword)); + + if (false === \strpos($sEmail, '@') || !\strlen($sPassword)) { + $this->loginErrorDelay(); + + throw new ClientException(Notifications::InvalidInputArgument); + } + + $this->Logger()->AddSecret($sPassword); + + $sLogin = $sEmail; + if ($this->Config()->Get('login', 'login_lowercase', true)) { + $sLogin = \MailSo\Base\Utils::StrToLowerIfAscii($sLogin); + } + + $this->Plugins()->RunHook('login.credentials', array(&$sEmail, &$sLogin, &$sPassword)); + + $this->Logger()->AddSecret($sPassword); + + $oAccount = null; + $sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', '')); + try { + $oAccount = $this->LoginProvide($sEmail, $sLogin, $sPassword, $sSignMeToken, $sClientCert, true); + + if (!$oAccount) { + throw new ClientException(Notifications::AuthError); + } + } catch (\Throwable $oException) { + $this->loginErrorDelay(); + $this->LoggerAuthHelper($oAccount, $this->getAdditionalLogParamsByUserLogin($sInputEmail)); + throw $oException; + } + + try { + $this->CheckMailConnection($oAccount, true); + } catch (\Throwable $oException) { + $this->loginErrorDelay(); + + throw $oException; + } + + return $oAccount; + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function GetAccountFromCustomToken(string $sToken, bool $bThrowExceptionOnFalse = true, bool $bValidateShortToken = true, bool $bQ = false): ?Account + { + $oResult = null; + if (!empty($sToken)) { + $aAccountHash = $bQ ? Utils::DecodeKeyValuesQ($sToken) : Utils::DecodeKeyValues($sToken); + if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0] && // simple token validation + 8 <= \count($aAccountHash) && // length checking + !empty($aAccountHash[7]) && // does short token exist + (!$bValidateShortToken || Utils::GetShortToken() === $aAccountHash[7] || // check short token if needed + (isset($aAccountHash[10]) && 0 < $aAccountHash[10] && \time() < $aAccountHash[10])) + ) { + $oAccount = $this->LoginProvide($aAccountHash[1], $aAccountHash[2], $aAccountHash[3], + empty($aAccountHash[5]) ? '' : $aAccountHash[5], empty($aAccountHash[11]) ? '' : $aAccountHash[11], $bThrowExceptionOnFalse); + + if ($oAccount) { + // init proxy user/password + if (!empty($aAccountHash[8]) && !empty($aAccountHash[9])) { + $oAccount->SetProxyAuthUser($aAccountHash[8]); + $oAccount->SetProxyAuthPassword($aAccountHash[9]); + } + + $this->Logger()->AddSecret($oAccount->Password()); + $this->Logger()->AddSecret($oAccount->ProxyAuthPassword()); + + $oAccount->SetParentEmail($aAccountHash[6]); + $oResult = $oAccount; + } + } else if ($bThrowExceptionOnFalse) { + throw new ClientException(Notifications::AuthError); + } + } + + if ($bThrowExceptionOnFalse && !$oResult) { + throw new ClientException(Notifications::AuthError); + } + + return $oResult; + } + + protected function LoginProvide(string $sEmail, string $sLogin, string $sPassword, string $sSignMeToken = '', string $sClientCert = '', bool $bThrowProvideException = false): ?Account + { + $oAccount = null; + if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) { + $oDomain = $this->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true); + if ($oDomain) { + if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) { + $oAccount = new Account($sEmail, $sLogin, $sPassword, $oDomain, $sSignMeToken, '', '', $sClientCert); + $this->Plugins()->RunHook('filter.account', array($oAccount)); + + if ($bThrowProvideException && !$oAccount) { + throw new ClientException(Notifications::AuthError); + } + } else if ($bThrowProvideException) { + throw new ClientException(Notifications::AccountNotAllowed); + } + } else if ($bThrowProvideException) { + throw new ClientException(Notifications::DomainNotAllowed); + } + } + + return $oAccount; + } + + public function GetSpecAuthTokenCookie(): string + { + return Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY, ''); + } + + public function GetAccountFromSignMeToken(): ?Account + { + $oAccount = null; + + $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY, ''); + if (!empty($sSignMeToken)) { + $aTokenData = Utils::DecodeKeyValuesQ($sSignMeToken); + if (!empty($aTokenData['e']) && !empty($aTokenData['t'])) { + $sTokenSettings = $this->StorageProvider()->Get($aTokenData['e'], + StorageType::CONFIG, + 'sign_me' + ); + + if (!empty($sTokenSettings)) { + $aSignMeData = Utils::DecodeKeyValuesQ($sTokenSettings); + if (!empty($aSignMeData['AuthToken']) && + !empty($aSignMeData['SignMeToken']) && + $aSignMeData['SignMeToken'] === $aTokenData['t']) { + $oAccount = $this->GetAccountFromCustomToken($aSignMeData['AuthToken'], false, false, true); + } + } + } + } else { + Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + } + + return $oAccount; + } + + // rlspecauth / AuthAccountHash + public function getAuthAccountHash() : string + { + if ('' === $this->sSpecAuthToken && !\strlen($this->GetSpecAuthLogoutTokenWithDeletion())) { + $sAuthAccountHash = $this->GetSpecAuthTokenCookie() ?: $this->GetSpecAuthToken(); + if (empty($sAuthAccountHash)) { + $oAccount = $this->GetAccountFromSignMeToken(); + if ($oAccount) try + { + $this->CheckMailConnection($oAccount); + $this->AuthToken($oAccount); + $sAuthAccountHash = $this->GetSpecAuthToken(); + } + catch (\Throwable $oException) + { + $oException = null; + $this->ClearSignMeData($oAccount); + } + } + $this->SetSpecAuthToken($sAuthAccountHash); + } + return $this->GetSpecAuthToken(); + } + + private function getLocalAuthToken(): string + { + $sToken = $this->GetSpecAuthToken(); + return !empty($sToken) && '_' === \substr($sToken, 0, 1) ? \substr($sToken, 1) : ''; + } + + public function GetShortLifeSpecAuthToken(int $iLife = 60): string + { + $aAccountHash = Utils::DecodeKeyValues($this->getLocalAuthToken()); + if (!empty($aAccountHash[0]) && 'token' === $aAccountHash[0]) { + $aAccountHash[10] = \time() + $iLife; + return Utils::EncodeKeyValues($aAccountHash); + } + + return ''; + } + + /** + * @throws \RainLoop\Exceptions\ClientException + */ + public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Model\Account + { + return $this->GetAccountFromCustomToken($this->getLocalAuthToken(), $bThrowExceptionOnFalse, true, true); + } + + public function GetSpecAuthToken(): string + { + return $this->sSpecAuthToken; + } + + public function SetSpecAuthToken(string $sSpecAuthToken): self + { + $this->sSpecAuthToken = $sSpecAuthToken; + + return $this; + } + + public function SetAuthToken(Account $oAccount): void + { + $sSpecAuthToken = '_' . $oAccount->GetAuthTokenQ(); + + $this->SetSpecAuthToken($sSpecAuthToken); + Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken); + + if ($oAccount->SignMe() && \strlen($oAccount->SignMeToken())) { + Utils::SetCookie(self::AUTH_SIGN_ME_TOKEN_KEY, + Utils::EncodeKeyValuesQ(array( + 'e' => $oAccount->Email(), + 't' => $oAccount->SignMeToken() + )), + \time() + 60 * 60 * 24 * 30); + + $this->StorageProvider()->Put($oAccount, + StorageType::CONFIG, + 'sign_me', + Utils::EncodeKeyValuesQ(array( + 'Time' => \time(), + 'AuthToken' => $oAccount->GetAuthTokenQ(), + 'SignMeToken' => $oAccount->SignMeToken() + )) + ); + } + } + + protected function ClearSignMeData(Account $oAccount) : void + { + if ($oAccount) { + Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + $this->StorageProvider()->Clear($oAccount, + StorageType::CONFIG, + 'sign_me' + ); + } + } + + public function GetSpecAuthLogoutTokenWithDeletion(): string + { + $sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, ''); + if (\strlen($sResult)) { + Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY); + } + + return $sResult; + } + + public function SetAuthLogoutToken(): void + { + \header('X-RainLoop-Action: Logout'); + Utils::SetCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY, \md5($_SERVER['REQUEST_TIME_FLOAT'])); + } + + public function GetSpecLogoutCustomMgsWithDeletion(): string + { + $sResult = Utils::GetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, ''); + if (\strlen($sResult)) { + Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY); + } + + return $sResult; + } + + public function SetSpecLogoutCustomMgsWithDeletion(string $sMessage): string + { + Utils::SetCookie(self::AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY, $sMessage); + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php index 48811d8c1..24c615e93 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Api.php @@ -205,7 +205,7 @@ class Api public static function LogoutCurrentLogginedUser() : bool { - Utils::ClearCookie('rlsession'); + Utils::ClearCookie(Utils::SHORT_TOKEN); return true; } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php index 02ec64105..1e8aa9198 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/Account.php @@ -152,7 +152,7 @@ class Account public function Hash() : string { - return md5(APP_SALT.$this->Email().APP_SALT.$this->DomainIncHost(). + return \md5(APP_SALT.$this->Email().APP_SALT.$this->DomainIncHost(). APP_SALT.$this->DomainIncPort().APP_SALT.$this->Password().APP_SALT.'0'.APP_SALT.$this->ParentEmail().APP_SALT); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php index c575104a3..bf1687384 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php @@ -14,6 +14,18 @@ class Utils */ static $CookieDefaultSecure = null; + const + /** + * Used by: ServiceProxyExternal, compileLogParams, GetCsrfToken + * To preven CSRF attacks on all requests. + */ + CONNECTION_TOKEN = 'rltoken', + + /** + * Used by: GetAuthToken, GetAuthTokenQ, GetAccountFromCustomToken, EncryptStringQ, DecryptStringQ + */ + SHORT_TOKEN = 'rlsession'; + public static function EncryptString(string $sString, string $sKey) : string { return \MailSo\Base\Crypt::Encrypt($sString, $sKey); @@ -37,43 +49,46 @@ class Utils public static function EncodeKeyValues(array $aValues, string $sCustomKey = '') : string { return \MailSo\Base\Utils::UrlSafeBase64Encode( - static::EncryptString(\serialize($aValues), \md5(APP_SALT.$sCustomKey))); + static::EncryptString(\json_encode($aValues), \md5(APP_SALT.$sCustomKey))); } public static function DecodeKeyValues(string $sEncodedValues, string $sCustomKey = '') : array { - $aResult = \unserialize( - static::DecryptString( - \MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedValues), \md5(APP_SALT.$sCustomKey))); - - return \is_array($aResult) ? $aResult : array(); + return static::unserialize( + static::DecryptString(\MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedValues), \md5(APP_SALT.$sCustomKey)) + ); } public static function EncodeKeyValuesQ(array $aValues, string $sCustomKey = '') : string { return \MailSo\Base\Utils::UrlSafeBase64Encode( static::EncryptStringQ( - \serialize($aValues), \md5(APP_SALT.$sCustomKey))); + \json_encode($aValues), \md5(APP_SALT.$sCustomKey))); } public static function DecodeKeyValuesQ(string $sEncodedValues, string $sCustomKey = '') : array { - $aResult = \unserialize( - static::DecryptStringQ( - \MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedValues), \md5(APP_SALT.$sCustomKey))); + return static::unserialize( + static::DecryptStringQ(\MailSo\Base\Utils::UrlSafeBase64Decode($sEncodedValues), \md5(APP_SALT.$sCustomKey)) + ); + } - return \is_array($aResult) ? $aResult : array(); + public static function unserialize(string $sDecodedValues) : array + { + try { + return \json_decode($sDecodedValues, true, 512, JSON_THROW_ON_ERROR) ?: array(); + } catch (\Throwable $e) { + return \unserialize($aResult) ?: array(); + } } public static function GetConnectionToken() : string { - $sKey = 'rltoken'; - - $sToken = static::GetCookie($sKey, null); + $sToken = static::GetCookie(self::CONNECTION_TOKEN, null); if (null === $sToken) { $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); - static::SetCookie($sKey, $sToken, \time() + 60 * 60 * 24 * 30); + static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 60 * 60 * 24 * 30); } return \md5('Connection'.APP_SALT.$sToken.'Token'.APP_SALT); @@ -86,13 +101,11 @@ class Utils public static function GetShortToken() : string { - $sKey = 'rlsession'; - - $sToken = static::GetCookie($sKey, null); + $sToken = static::GetCookie(self::SHORT_TOKEN, null); if (!$sToken) { $sToken = \MailSo\Base\Utils::Sha1Rand(APP_SALT); - static::SetCookie($sKey, $sToken, 0); + static::SetCookie(self::SHORT_TOKEN, $sToken, 0); } return \md5('Session'.APP_SALT.$sToken.'Token'.APP_SALT); @@ -100,12 +113,10 @@ class Utils public static function UpdateConnectionToken() : void { - $sKey = 'rltoken'; - - $sToken = static::GetCookie($sKey, ''); + $sToken = static::GetCookie(self::CONNECTION_TOKEN, ''); if (!empty($sToken)) { - static::SetCookie($sKey, $sToken, \time() + 60 * 60 * 24 * 30); + static::SetCookie(self::CONNECTION_TOKEN, $sToken, \time() + 60 * 60 * 24 * 30); } }