From a18d393f54e2cea965fe1f490fd6fd89b05e5f9a Mon Sep 17 00:00:00 2001 From: djmaze Date: Thu, 11 Nov 2021 13:34:41 +0100 Subject: [PATCH] Revamp the whole accounts system for better management and control. This also solves RainLoop/#2134 --- .../0.0.0/app/libraries/MailSo/Base/Utils.php | 3 +- .../0.0.0/app/libraries/RainLoop/Actions.php | 63 ++--- .../libraries/RainLoop/Actions/Accounts.php | 145 ++++++------ .../libraries/RainLoop/Actions/Contacts.php | 43 +++- .../libraries/RainLoop/Actions/Messages.php | 2 +- .../app/libraries/RainLoop/Actions/User.php | 4 +- .../libraries/RainLoop/Actions/UserAuth.php | 217 +++++++++--------- .../app/libraries/RainLoop/Model/Account.php | 63 +---- .../RainLoop/Model/AdditionalAccount.php | 73 ++++++ .../RainLoop/Providers/Files/FileStorage.php | 9 +- .../Providers/Storage/FileStorage.php | 4 +- .../app/libraries/RainLoop/ServiceActions.php | 21 +- .../v/0.0.0/app/libraries/RainLoop/Utils.php | 17 ++ .../0.0.0/app/libraries/snappymail/crypt.php | 112 ++++----- .../templates/Views/User/PopupsAccount.html | 48 ++-- 15 files changed, 432 insertions(+), 392 deletions(-) create mode 100644 snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php index b311b2005d..03b8cdafac 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Base/Utils.php @@ -1164,8 +1164,7 @@ public static function UrlSafeBase64Encode(string $sValue) : string public static function UrlSafeBase64Decode(string $sValue) : string { - $sValue = \rtrim(\strtr($sValue, '-_.', '+/='), '='); - return static::Base64Decode(\str_pad($sValue, \strlen($sValue) + (\strlen($sValue) % 4), '=', STR_PAD_RIGHT)); + return \base64_decode(\strtr($sValue, '-_', '+/'), '='); } public static function ParseFetchSequence(string $sSequence) : array 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 1f87c32a19..d5c29e03eb 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions.php @@ -31,6 +31,11 @@ class Actions */ const AUTH_SPEC_TOKEN_KEY = 'smaccount'; + /** + * This session cookie optionally contains a \RainLoop\Model\AdditionalAccount + */ + const AUTH_ADDITIONAL_TOKEN_KEY = 'smadditional'; + const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout'; const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk'; @@ -553,7 +558,7 @@ public function Cacher(?Model\Account $oAccount = null, bool $bForceFile = false { $sKey = ''; if ($oAccount) { - $sKey = $oAccount->ParentEmailHelper(); + $sKey = $this->GetMainEmail($oAccount); } $sIndexKey = empty($sKey) ? '_default_' : $sKey; @@ -918,7 +923,9 @@ public function AppData(bool $bAdmin): array $aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true); } - $aResult['ParentEmail'] = $oAccount->ParentEmail(); + if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { + $aResult['ParentEmail'] = $oAccount->ParentEmail(); + } $oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount); @@ -1128,32 +1135,6 @@ public function GetIdentityByID(Model\Account $oAccount, string $sID, bool $bFir return $bFirstOnEmpty && isset($aIdentities[0]) ? $aIdentities[0] : null; } - /** - * @throws \MailSo\Base\Exceptions\Exception - */ - public function getAccountUnreadCountFromHash(string $sHash): int - { - $iResult = 0; - - $oAccount = $this->GetAccountFromCustomToken($sHash); - if ($oAccount) { - try { - $oMailClient = new \MailSo\Mail\MailClient(); - $oMailClient->SetLogger($this->Logger()); - - $oAccount->IncConnectAndLoginHelper($this->Plugins(), $oMailClient, $this->Config()); - - $iResult = $oMailClient->InboxUnreadCount(); - - $oMailClient->Disconnect(); - } catch (\Throwable $oException) { - $this->Logger()->WriteException($oException); - } - } - - return $iResult; - } - public function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void { $sValue = $this->GetActionParam($sParamName, ''); @@ -1420,28 +1401,10 @@ private function importContactsFromCsvFile(Model\Account $oAccount, /*resource*/ if (\count($aData)) { $this->Logger()->Write('Import contacts from csv'); - $iCount = $oAddressBookProvider->ImportCsvArray($oAccount->ParentEmailHelper(), $aData); - } - } - } - - return $iCount; - } - - private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int - { - $iCount = 0; - if ($oAccount && \is_resource($rFile)) { - $oAddressBookProvider = $this->AddressBookProvider($oAccount); - if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { - $sFile = \stream_get_contents($rFile); - if (\is_resource($rFile)) { - \fclose($rFile); - } - - if (is_string($sFile) && 5 < \strlen($sFile)) { - $this->Logger()->Write('Import contacts from vcf'); - $iCount = $oAddressBookProvider->ImportVcfFile($oAccount->ParentEmailHelper(), $sFile); + $iCount = $oAddressBookProvider->ImportCsvArray( + $this->GetMainEmail($oAccount), + $aData + ); } } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php index 2b15ed5ec8..69cd4407c9 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Accounts.php @@ -8,67 +8,54 @@ use RainLoop\Model\Identity; use RainLoop\Notifications; use RainLoop\Providers\Storage\Enumerations\StorageType; +use RainLoop\Utils; trait Accounts { + protected function GetMainEmail(Account $oAccount) + { + return $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email(); + } + public function GetAccounts(Account $oAccount): array { + if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) { + throw new \LogicException('Only main account can have sub accounts'); + } if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { $sAccounts = $this->StorageProvider()->Get($oAccount, StorageType::CONFIG, - 'accounts' + 'additionalaccounts' ); $aAccounts = $sAccounts ? \json_decode($sAccounts, true) : array(); - if (\is_array($aAccounts) && \count($aAccounts)) { - if (1 === \count($aAccounts)) { - $this->SetAccounts($oAccount, array()); - - } else if (1 < \count($aAccounts)) { - $sOrder = $this->StorageProvider()->Get($oAccount, - StorageType::CONFIG, - 'accounts_identities_order' - ); - - $aOrder = empty($sOrder) ? array() : \json_decode($sOrder, true); - if (isset($aOrder['Accounts']) && \is_array($aOrder['Accounts']) && 1 < \count($aOrder['Accounts'])) { - $aAccounts = \array_merge(\array_flip($aOrder['Accounts']), $aAccounts); - - $aAccounts = \array_filter($aAccounts, function ($sHash) { - return 5 < \strlen($sHash); - }); - } - } - + if ($aAccounts && \is_array($aAccounts)) { return $aAccounts; } } - $aAccounts = array(); - if (!$oAccount->IsAdditionalAccount()) { - $aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken(); - } - - return $aAccounts; + return array(); } protected function SetAccounts(Account $oAccount, array $aAccounts = array()): void { - $sParentEmail = $oAccount->ParentEmailHelper(); - if (!$aAccounts || - (1 === \count($aAccounts) && !empty($aAccounts[$sParentEmail]))) { + if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) { + throw new \LogicException('Only main account can have sub accounts'); + } + $sParentEmail = $oAccount->Email(); + if (!$aAccounts) { $this->StorageProvider()->Clear( $oAccount, StorageType::CONFIG, - 'accounts' + 'additionalaccounts' ); } else { $this->StorageProvider()->Put( $oAccount, StorageType::CONFIG, - 'accounts', + 'additionalaccounts', \json_encode($aAccounts) ); } @@ -79,36 +66,30 @@ protected function SetAccounts(Account $oAccount, array $aAccounts = array()): v */ public function DoAccountSetup(): array { - $oAccount = $this->getAccountFromToken(); + $oMainAccount = $this->getMainAccountFromToken(); - if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { + if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) { return $this->FalseResponse(__FUNCTION__); } - $sParentEmail = $oAccount->ParentEmailHelper(); - - $aAccounts = $this->GetAccounts($oAccount); + $aAccounts = $this->GetAccounts($oMainAccount); $sEmail = \trim($this->GetActionParam('Email', '')); $sPassword = $this->GetActionParam('Password', ''); $bNew = '1' === (string)$this->GetActionParam('New', '1'); $sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); - if ($bNew && ($oAccount->Email() === $sEmail || $sParentEmail === $sEmail || isset($aAccounts[$sEmail]))) { + if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) { throw new ClientException(Notifications::AccountAlreadyExists); } else if (!$bNew && !isset($aAccounts[$sEmail])) { throw new ClientException(Notifications::AccountDoesNotExist); } - $oNewAccount = $this->LoginProcess($sEmail, $sPassword); - $oNewAccount->SetParentEmail($sParentEmail); + $oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, $oMainAccount); - $aAccounts[$oNewAccount->Email()] = $oNewAccount->GetAuthToken(); - if (!$oAccount->IsAdditionalAccount()) { - $aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken(); - } + $aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount); + $this->SetAccounts($oMainAccount, $aAccounts); - $this->SetAccounts($oAccount, $aAccounts); return $this->TrueResponse(__FUNCTION__); } @@ -117,31 +98,29 @@ public function DoAccountSetup(): array */ public function DoAccountDelete(): array { - $oAccount = $this->getAccountFromToken(); + $oMainAccount = $this->getMainAccountFromToken(); - if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { + if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) { return $this->FalseResponse(__FUNCTION__); } - $sParentEmail = $oAccount->ParentEmailHelper(); $sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', '')); $sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true); - $aAccounts = $this->GetAccounts($oAccount); + $aAccounts = $this->GetAccounts($oMainAccount); - if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete])) { - unset($aAccounts[$sEmailToDelete]); - - $oAccountToChange = null; - if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail])) { - $oAccountToChange = $this->GetAccountFromCustomToken($aAccounts[$sParentEmail]); - if ($oAccountToChange) { - $this->SetAuthToken($oAccountToChange); - } + if (\strlen($sEmailToDelete) && isset($aAccounts[$sEmailToDelete])) { + $bReload = false; +// $oAccount = $this->getAccountFromToken(); + if ($oAccount->Email() === $sEmailToDelete) { + Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + $bReload = true; } - $this->SetAccounts($oAccount, $aAccounts); - return $this->TrueResponse(__FUNCTION__, array('Reload' => !!$oAccountToChange)); + unset($aAccounts[$sEmailToDelete]); + $this->SetAccounts($oMainAccount, $aAccounts); + + return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload)); } return $this->FalseResponse(__FUNCTION__); @@ -188,8 +167,6 @@ public function DoIdentityDelete(): array */ public function DoAccountsAndIdentitiesSortOrder(): array { - $oAccount = $this->getAccountFromToken(); - $aAccounts = $this->GetActionParam('Accounts', null); $aIdentities = $this->GetActionParam('Identities', null); @@ -197,10 +174,20 @@ public function DoAccountsAndIdentitiesSortOrder(): array return $this->FalseResponse(__FUNCTION__); } - return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put($oAccount, - StorageType::CONFIG, 'accounts_identities_order', + $oAccount = $this->getMainAccountFromToken(); + if (1 < \count($aAccounts)) { + $aAccounts = \array_filter(\array_merge( + \array_fill_keys($aOrder['Accounts'], null), + $this->GetAccounts($oAccount) + )); + $this->SetAccounts($oAccount, $aAccounts); + } + + return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put( + $this->getMainAccountFromToken(), + StorageType::CONFIG, + 'identities_order', \json_encode(array( - 'Accounts' => \is_array($aAccounts) ? $aAccounts : array(), 'Identities' => \is_array($aIdentities) ? $aIdentities : array() )) )); @@ -211,39 +198,39 @@ public function DoAccountsAndIdentitiesSortOrder(): array */ public function DoAccountsAndIdentities(): array { - $oAccount = $this->getAccountFromToken(); - - $mAccounts = false; + $oAccount = $this->getMainAccountFromToken(); + $aAccounts = false; if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) { - $mAccounts = $this->GetAccounts($oAccount); - $mAccounts = \array_keys($mAccounts); - - foreach ($mAccounts as $iIndex => $sName) { - $mAccounts[$iIndex] = \MailSo\Base\Utils::IdnToUtf8($sName); + $aAccounts = \array_map( + 'MailSo\\Base\\Utils::IdnToUtf8', + \array_keys($this->GetAccounts($oAccount)) + ); + if ($aAccounts) { + \array_unshift($aAccounts, \MailSo\Base\Utils::IdnToUtf8($oAccount->Email())); } } return $this->DefaultResponse(__FUNCTION__, array( - 'Accounts' => $mAccounts, + 'Accounts' => $aAccounts, 'Identities' => $this->GetIdentities($oAccount) )); } /** - * @param Account $account + * @param Account $oAccount * @return Identity[] */ - public function GetIdentities(Account $account): array + public function GetIdentities(Account $oAccount): array { // A custom name for a single identity is also stored in this system - $allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $account); + $allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $oAccount); // Get all identities - $identities = $this->IdentitiesProvider()->GetIdentities($account, $allowMultipleIdentities); + $identities = $this->IdentitiesProvider()->GetIdentities($oAccount, $allowMultipleIdentities); // Sort identities - $orderString = $this->StorageProvider()->Get($account, StorageType::CONFIG, 'accounts_identities_order'); + $orderString = $this->StorageProvider()->Get($oAccount, StorageType::CONFIG, 'identities_order'); $order = \json_decode($orderString, true) ?? []; if (isset($order['Identities']) && \is_array($order['Identities']) && \count($order['Identities']) > 1) { $list = \array_map(function ($item) { diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php index bfda2d82fd..f60abe3cd2 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Contacts.php @@ -5,6 +5,11 @@ trait Contacts { + private function GetAccountParentEmail() + { +// $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email() + } + public function DoSaveContactsSyncData() : array { $oAccount = $this->getAccountFromToken(); @@ -45,7 +50,7 @@ public function DoContactsSync() : array if (isset($mData['Enable'], $mData['User'], $mData['Password'], $mData['Url']) && $mData['Enable']) { $bResult = $oAddressBookProvider->Sync( - $oAccount->ParentEmailHelper(), + $this->GetMainEmail($oAccount), $mData['Url'], $mData['User'], $mData['Password']); } } @@ -75,7 +80,7 @@ public function DoContacts() : array if ($oAbp->IsActive()) { $iResultCount = 0; - $mResult = $oAbp->GetContacts($oAccount->ParentEmailHelper(), + $mResult = $oAbp->GetContacts($this->GetMainEmail($oAccount), $iOffset, $iLimit, $sSearch, $iResultCount); } @@ -98,7 +103,7 @@ public function DoContactsDelete() : array $bResult = false; if (0 < \count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive()) { - $bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($oAccount->ParentEmailHelper(), $aFilteredUids); + $bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($this->GetMainEmail($oAccount), $aFilteredUids); } return $this->DefaultResponse(__FUNCTION__, $bResult); @@ -119,7 +124,7 @@ public function DoContactSave() : array $oContact = null; if (0 < \strlen($sUid)) { - $oContact = $oAddressBookProvider->GetContactByID($oAccount->ParentEmailHelper(), $sUid); + $oContact = $oAddressBookProvider->GetContactByID($this->GetMainEmail($oAccount), $sUid); } if (!$oContact) @@ -159,7 +164,7 @@ public function DoContactSave() : array $oContact->PopulateDisplayAndFullNameValue(true); - $bResult = $oAddressBookProvider->ContactSave($oAccount->ParentEmailHelper(), $oContact); + $bResult = $oAddressBookProvider->ContactSave($this->GetMainEmail($oAccount), $oContact); } return $this->DefaultResponse(__FUNCTION__, array( @@ -300,7 +305,7 @@ public function RawContactsVcf() : bool $this->oHttp->ServerNoCache(); return $this->AddressBookProvider($oAccount)->IsActive() ? - $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'vcf') : false; + $this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'vcf') : false; } public function RawContactsCsv() : bool @@ -315,7 +320,31 @@ public function RawContactsCsv() : bool $this->oHttp->ServerNoCache(); return $this->AddressBookProvider($oAccount)->IsActive() ? - $this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'csv') : false; + $this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'csv') : false; + } + + private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int + { + $iCount = 0; + if ($oAccount && \is_resource($rFile)) { + $oAddressBookProvider = $this->AddressBookProvider($oAccount); + if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { + $sFile = \stream_get_contents($rFile); + if (\is_resource($rFile)) { + \fclose($rFile); + } + + if (is_string($sFile) && 5 < \strlen($sFile)) { + $this->Logger()->Write('Import contacts from vcf'); + $iCount = $oAddressBookProvider->ImportVcfFile( + $this->GetMainEmail($oAccount), + $sFile + ); + } + } + } + + return $iCount; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index 668c39c6d4..5d83165908 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -324,7 +324,7 @@ public function DoSendMessage() : array $oSettings = $this->SettingsProvider()->Load($oAccount); $this->AddressBookProvider($oAccount)->IncFrec( - $oAccount->ParentEmailHelper(), \array_values($aArrayToFrec), + $this->GetMainEmail($oAccount), \array_values($aArrayToFrec), !!$oSettings->GetConf('ContactsAutosave', !!$oConfig->Get('defaults', 'contacts_autosave', true))); } 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 b3150f139f..ee5740f814 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 @@ -203,7 +203,7 @@ public function DoLogout() : array $oAccount = $this->getAccountFromToken(false); if ($oAccount) { - if (!$oAccount->IsAdditionalAccount()) + if (!\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) { $this->ClearSignMeData(); Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); @@ -393,7 +393,7 @@ public function DoSuggestions() : array $oAddressBookProvider = $this->AddressBookProvider($oAccount); if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) { - $aSuggestions = $oAddressBookProvider->GetSuggestions($oAccount->ParentEmailHelper(), $sQuery, $iLimit); + $aSuggestions = $oAddressBookProvider->GetSuggestions($this->GetMainEmail($oAccount), $sQuery, $iLimit); if (!\count($aResult)) { $aResult = $aSuggestions; 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 index 0bdd5c3eee..59a8b0189c 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/UserAuth.php @@ -5,6 +5,7 @@ use RainLoop\Notifications; use RainLoop\Utils; use RainLoop\Model\Account; +use RainLoop\Model\AdditionalAccount; use RainLoop\Providers\Storage\Enumerations\StorageType; use RainLoop\Exceptions\ClientException; @@ -13,18 +14,13 @@ trait UserAuth /** * @var string */ - private $sSpecAuthToken = null; - private $oSpecAuthAccount = null; - - /** - * Or use 'aes-256-xts' ? - */ - private static $sCipher = 'aes-256-cbc-hmac-sha1'; + private $oAdditionalAuthAccount = null; + private $oMainAuthAccount = null; /** * @throws \RainLoop\Exceptions\ClientException */ - public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false): Account + public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false, Account $oMainAccount = null): Account { $sInputEmail = $sEmail; @@ -117,7 +113,14 @@ public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe $oAccount = null; $sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', '')); try { - $oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true); + if ($oMainAccount) { + $oAccount = AdditionalAccount::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true); + if ($oAccount) { + $oAccount->SetParentEmail($oMainAccount->Email()); + } + } else { + $oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true); + } if (!$oAccount) { throw new ClientException(Notifications::AuthError); @@ -141,86 +144,103 @@ public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe } /** + * Returns RainLoop\Model\AdditionalAccount when it exists, + * else returns RainLoop\Model\Account when it exists, + * else null + * * @throws \RainLoop\Exceptions\ClientException */ - public function GetAccountFromCustomToken(string $sToken): ?Account + public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account { - return empty($sToken) ? null : Account::NewInstanceFromAuthToken($sToken); + if (\is_null($this->oAdditionalAuthAccount) && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) { + $aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + if ($aData) { + $this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray( + $this, + $aData, + $bThrowExceptionOnFalse + ); + } + if (!$this->oAdditionalAuthAccount) { + $this->oAdditionalAuthAccount = false; + Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); + } + } + return $this->oAdditionalAuthAccount ?: $this->getMainAccountFromToken($bThrowExceptionOnFalse); } /** + * Returns RainLoop\Model\Account when it exists, else null + * * @throws \RainLoop\Exceptions\ClientException */ - public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account + public function getMainAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account { - if (!$this->oSpecAuthAccount) { - if (!\is_string($this->sSpecAuthToken)) { - $this->sSpecAuthToken = ''; - if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) { - Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY); - Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); -// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); - } else { - $sAuthAccountHash = Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY); - if (empty($sAuthAccountHash)) { - $oAccount = $this->GetAccountFromSignMeToken(); - if ($oAccount) { - $this->SetAuthToken($oAccount); - } - } else { - $this->sSpecAuthToken = $sAuthAccountHash; - } - } + if (!$this->oMainAuthAccount) { + if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) { + Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY); + Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); +// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY); +// Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); } - $aData = \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($this->sSpecAuthToken)); - if (!empty($aData)) { - $this->oSpecAuthAccount = Account::NewInstanceFromTokenArray( + $aData = Utils::GetSecureCookie(self::AUTH_SPEC_TOKEN_KEY); + if ($aData) { + $this->oMainAuthAccount = Account::NewInstanceFromTokenArray( $this, $aData, $bThrowExceptionOnFalse ); + } else { + $oAccount = $this->GetAccountFromSignMeToken(); + if ($oAccount) { + $this->SetAuthToken($oAccount); + } } - if ($bThrowExceptionOnFalse && !$this->oSpecAuthAccount) { + if ($bThrowExceptionOnFalse && !$this->oMainAuthAccount) { throw new ClientException(\RainLoop\Notifications::AuthError); } } - return $this->oSpecAuthAccount; + return $this->oMainAuthAccount; } public function SetAuthToken(Account $oAccount): void { - $sSpecAuthToken = \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount)); - - $this->sSpecAuthToken = $sSpecAuthToken; - $this->oSpecAuthAccount = null; - Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken); - - if (isset($aAccounts[$oAccount->Email()])) { - $aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken(); - $this->SetAccounts($oAccount, $aAccounts); + if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) { + throw new \LogicException('Only main Account can be set as AuthToken'); } + $this->oAdditionalAuthAccount = false; + $this->oMainAuthAccount = $oAccount; + Utils::SetSecureCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount); } - private static function GetSignMeToken(): ?array + public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void { - $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY); - if (empty($sSignMeToken)) { - return null; + $this->oAdditionalAuthAccount = $oAccount ?: false; + if ($oAccount) { + Utils::SetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount); + } else { + Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY); } - $aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken); - return \is_array($aResult) ? $aResult : null; } - private static function SetSignMeTokenCookie(array $aData): void + /** + * SignMe methods used for the "remember me" cookie + */ + + private static function GetSignMeToken(): ?array { - Utils::SetCookie( - self::AUTH_SIGN_ME_TOKEN_KEY, - \SnappyMail\Crypt::EncryptUrlSafe($aData), - \time() + 3600 * 24 * 30 // 30 days - ); + $sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + if ($sSignMeToken) { + $aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken); + if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) { + return $aResult; + } + Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); + } + return null; } private function SetSignMeToken(Account $oAccount): void @@ -230,25 +250,15 @@ private function SetSignMeToken(Account $oAccount): void $uuid = \SnappyMail\UUID::generate(); $data = \SnappyMail\Crypt::Encrypt($oAccount); - if ('xxtea' === $data[0]) { - static::SetSignMeTokenCookie(array( - 'e' => $oAccount->Email(), - 'u' => $uuid, - 'x' => \base64_encode($data[1]) - )); - } else if ('sodium' === $data[0]) { - static::SetSignMeTokenCookie(array( - 'e' => $oAccount->Email(), - 'u' => $uuid, - 's' => \base64_encode($data[1]) - )); - } else { - static::SetSignMeTokenCookie(array( + Utils::SetCookie( + self::AUTH_SIGN_ME_TOKEN_KEY, + \SnappyMail\Crypt::EncryptUrlSafe([ 'e' => $oAccount->Email(), 'u' => $uuid, - 'o' => \base64_encode($data[1]) - )); - } + $data[0] => \base64_encode($data[1]) + ]), + \time() + 3600 * 24 * 30 // 30 days + ); $this->StorageProvider()->Put( $oAccount, @@ -261,58 +271,55 @@ private function SetSignMeToken(Account $oAccount): void public function GetAccountFromSignMeToken(): ?Account { $aTokenData = static::GetSignMeToken(); - if (!empty($aTokenData)) { - $oAccount = null; - if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) { + if ($aTokenData) { + try + { $sAuthToken = $this->StorageProvider()->Get( $aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u'] ); - if (empty($sAuthToken)) { - return null; - } - if (!empty($aTokenData['x'])) { - $aAccountHash = \SnappyMail\Crypt::XxteaDecrypt($sAuthToken, \base64_decode($aTokenData['x'])); - } else if (!empty($aTokenData['s'])) { - $aAccountHash = \SnappyMail\Crypt::SodiumDecrypt($sAuthToken, \base64_decode($aTokenData['s'])); - } else if (!empty($aTokenData['o'])) { - $aAccountHash = \SnappyMail\Crypt::OpenSSLDecrypt($sAuthToken, \base64_decode($aTokenData['o'])); - } - if (!empty($aAccountHash) && \is_array($aAccountHash)) { - $oAccount = Account::NewInstanceFromTokenArray($this, $aAccountHash); + if ($sAuthToken) { + $aAccountHash = \SnappyMail\Crypt::Decrypt([ + \array_key_last($aTokenData), + \base64_decode(\end($aTokenData)), + $sAuthToken + ]); + + $oAccount = \is_array($aAccountHash) + ? Account::NewInstanceFromTokenArray($this, $aAccountHash) : null; if ($oAccount) { - try - { - $this->CheckMailConnection($oAccount); - // Update lifetime - $this->SetSignMeToken($oAccount); - - return $oAccount; - } - catch (\Throwable $oException) - { - } + $this->CheckMailConnection($oAccount); + // Update lifetime + $this->SetSignMeToken($oAccount); + + return $oAccount; } } } + catch (\Throwable $oException) + { + } + + $this->ClearSignMeData(); } - $this->ClearSignMeData(); return null; } protected function ClearSignMeData() : void { - if (isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY])) { - $aTokenData = static::GetSignMeToken(); - if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) { - $this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']); - } + $aTokenData = static::GetSignMeToken(); + if ($aTokenData) { + $this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']); Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY); } } + /** + * Logout methods + */ + public function SetAuthLogoutToken(): void { \header('X-RainLoop-Action: Logout'); 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 473a1f5d94..7a4f7b4b0b 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 @@ -42,21 +42,11 @@ class Account implements \JsonSerializable */ private $oDomain; - /** - * @var string - */ - private $sParentEmail = ''; - public function Email() : string { return $this->sEmail; } - public function ParentEmail() : string - { - return $this->sParentEmail; - } - public function ProxyAuthUser() : string { return $this->sProxyAuthUser; @@ -67,16 +57,6 @@ public function ProxyAuthPassword() : string return $this->sProxyAuthPassword; } - public function ParentEmailHelper() : string - { - return $this->sParentEmail ?: $this->sEmail; - } - - public function IsAdditionalAccount() : bool - { - return !empty($this->sParentEmail); - } - public function IncLogin() : string { $sLogin = $this->sLogin; @@ -135,8 +115,7 @@ public function Hash() : string $this->sEmail, $this->Domain()->IncHost(), $this->Domain()->IncPort(), - $this->sPassword, - $this->sParentEmail + $this->sPassword ])); } @@ -145,11 +124,6 @@ public function SetPassword(string $sPassword) : void $this->sPassword = $sPassword; } - public function SetParentEmail(string $sParentEmail) : void - { - $this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true)); - } - public function SetProxyAuthUser(string $sProxyAuthUser) : void { $this->sProxyAuthUser = $sProxyAuthUser; @@ -168,29 +142,15 @@ public function jsonSerialize() $this->sLogin, // 2 $this->sPassword, // 3 $this->sClientCert, // 4 - $this->sParentEmail, // 5 - $this->sProxyAuthUser, // 6 - $this->sProxyAuthPassword // 7 - ); - } - - public function GetAuthToken() : string - { - return Utils::EncodeKeyValues($this->jsonSerialize()); -/* - return \MailSo\Base\Utils::UrlSafeBase64Encode( - \MailSo\Base\Crypt::Encrypt( - \json_encode($this), - \md5(APP_SALT.$sCustomKey) - ) + $this->sProxyAuthUser, // 5 + $this->sProxyAuthPassword // 6 ); -*/ } /** * @throws \RainLoop\Exceptions\ClientException */ - public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?Account + public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?self { return empty($sToken) ? null : static::NewInstanceFromTokenArray( @@ -200,14 +160,16 @@ public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, str ); } - public static function NewInstanceByLogin(\RainLoop\Actions $oActions, string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '', bool $bThrowException = false): ?self + public static function NewInstanceByLogin(\RainLoop\Actions $oActions, + string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '', + bool $bThrowException = false): ?self { $oAccount = null; if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) { $oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true); if ($oDomain) { if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) { - $oAccount = new self; + $oAccount = new static; $oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true); $oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin); @@ -236,7 +198,7 @@ public static function NewInstanceFromTokenArray( array $aAccountHash, bool $bThrowExceptionOnFalse = false): ?self { - if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 === \count($aAccountHash)) { + if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 === \count($aAccountHash)) { $oAccount = static::NewInstanceByLogin( $oActions, $aAccountHash[1], @@ -248,15 +210,14 @@ public static function NewInstanceFromTokenArray( if ($oAccount) { // init proxy user/password - if (!empty($aAccountHash[6]) && !empty($aAccountHash[7])) { - $oAccount->SetProxyAuthUser($aAccountHash[6]); - $oAccount->SetProxyAuthPassword($aAccountHash[7]); + if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) { + $oAccount->SetProxyAuthUser($aAccountHash[5]); + $oAccount->SetProxyAuthPassword($aAccountHash[6]); } $oActions->Logger()->AddSecret($oAccount->Password()); $oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword()); - $oAccount->SetParentEmail($aAccountHash[5]); return $oAccount; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php new file mode 100644 index 0000000000..634ba279d1 --- /dev/null +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Model/AdditionalAccount.php @@ -0,0 +1,73 @@ +sParentEmail; + } + + public function SetParentEmail(string $sParentEmail) : void + { + $this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true)); + } + + public function PasswordHash() : string + { + throw new \LogicException('Not allowed on AdditionalAccount'); + } + + public function Hash() : string + { + return \md5(parent::Hash() . $this->sParentEmail); + } + + public function jsonSerialize() + { + $aData = parent::jsonSerialize(); + $aData[] = $this->sParentEmail; + return $aData; + } + + public function asTokenArray(Account $oMainAccount) : array + { + $sHash = $oMainAccount->PasswordHash(); + $aData = $this->jsonSerialize(); + $aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash); + $aData[] = \hash_hmac('sha1', $aData[3], $sHash); + return $aData; + } + + public static function NewInstanceFromTokenArray( + \RainLoop\Actions $oActions, + array $aAccountHash, + bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/ + { + $iCount = \count($aAccountHash); + if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 <= $iCount && 9 >= $iCount) { + $sHash = $oActions->getMainAccountFromToken()->PasswordHash(); + $sPasswordHMAC = (8 < $iCount) ? \array_pop($aAccountHash) : null; + $sParentEmail = \array_pop($aAccountHash); + if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) { + $aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash); + } + $oAccount = parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse); + if ($oAccount) { + $oAccount->SetParentEmail($sParentEmail); + return $oAccount; + } + } + return null; + } + +} diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php index f73cc3bce6..a95ff8381e 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Files/FileStorage.php @@ -142,8 +142,13 @@ private static function fixName($filename) private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string { - $sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld'; - $sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : ''; + if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) { + $sEmail = $oAccount->ParentEmail(); + $sSubEmail = $oAccount->Email(); + } else { + $sEmail = $oAccount->Email() ?: 'nobody@unknown.tld'; + $sSubEmail = ''; + } $aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld'); $sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : ''); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php index 95976446c1..38004952bf 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Providers/Storage/FileStorage.php @@ -103,8 +103,8 @@ protected function generateFileName($mAccount, int $iStorageType, string $sKey, if (null === $mAccount) { $iStorageType = StorageType::NOBODY; } else if ($mAccount instanceof \RainLoop\Model\Account) { - $sEmail = $mAccount->ParentEmailHelper(); - if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction) + $sEmail = $mAccount instanceof \RainLoop\Model\AdditionalAccount ? $mAccount->ParentEmail() : $mAccount->Email(); + if ($this->bLocal && $mAccount instanceof \RainLoop\Model\AdditionalAccount && !$bForDeleteAction) { $sSubEmail = $mAccount->Email(); } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php index 2715053d63..110ce806a1 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/ServiceActions.php @@ -811,14 +811,16 @@ public function ServiceExternalLogin() : string return ''; } + /** + * Switch to AdditionalAccount + */ public function ServiceChange() : string { $this->oHttp->ServerNoCache(); -// $oAccount = $this->oActions->getAccountFromToken(false); - $oAccount = $this->oActions->GetAccount(); + $oMainAccount = $this->oActions->getMainAccountFromToken(false); - if ($oAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oAccount)) + if ($oMainAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) { $oAccountToLogin = null; $sEmail = empty($this->aPaths[2]) ? '' : \urldecode(\trim($this->aPaths[2])); @@ -826,17 +828,16 @@ public function ServiceChange() : string { $sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail); - $aAccounts = $this->oActions->GetAccounts($oAccount); + $aAccounts = $this->oActions->GetAccounts($oMainAccount); if (isset($aAccounts[$sEmail])) { - $oAccountToLogin = $this->oActions->GetAccountFromCustomToken($aAccounts[$sEmail]); + $oAccountToLogin = \RainLoop\Model\AdditionalAccount::NewInstanceFromTokenArray( + $this->oActions, + $aAccounts[$sEmail] + ); } } - - if ($oAccountToLogin) - { - $this->oActions->SetAuthToken($oAccountToLogin); - } + $this->oActions->SetAdditionalAuthToken($oAccountToLogin); } $this->oActions->Location('./'); 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 fad4f13299..432b46100c 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Utils.php @@ -147,6 +147,13 @@ public static function GetCookie(string $sName, $mDefault = null) return isset($_COOKIE[$sName]) ? $_COOKIE[$sName] : $mDefault; } + public static function GetSecureCookie(string $sName) + { + return isset($_COOKIE[$sName]) + ? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($_COOKIE[$sName])) + : null; + } + public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0, bool $bHttpOnly = true) { $sPath = static::$CookieDefaultPath; @@ -161,6 +168,16 @@ public static function SetCookie(string $sName, string $sValue = '', int $iExpir )); } + public static function SetSecureCookie(string $sName, $mValue, int $iExpire = 0, bool $bHttpOnly = true) + { + static::SetCookie( + $sName, + \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($mValue)), + $iExpire, + true + ); + } + public static function ClearCookie(string $sName) { if (isset($_COOKIE[$sName])) { diff --git a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php index f667e535a0..02fe2b01c3 100644 --- a/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php +++ b/snappymail/v/0.0.0/app/libraries/snappymail/crypt.php @@ -28,8 +28,8 @@ public static function listCiphers() : array public static function setCipher(string $cipher) : bool { if ($cipher) { - $aCiphers = static::listCiphers(); - if (\in_array($cipher, $aCiphers)) { + $ciphers = static::listCiphers(); + if (\in_array($cipher, $ciphers)) { static::$cipher = $cipher; return true; } @@ -48,63 +48,60 @@ private static function Passphrase(?string $key) : string ); } - public static function DecryptUrlSafe($sData, string $key = null) /* : mixed */ - { - return \json_decode( - \zlib_decode( - static::XxteaDecrypt( - \MailSo\Base\Utils::UrlSafeBase64Decode($sData), - static::Passphrase($key) - ) - ), - true - ); - } - - public static function EncryptUrlSafe($mData, string $key = null) : string - { - return \MailSo\Base\Utils::UrlSafeBase64Encode( - static::XxteaEncrypt( - \zlib_encode( - \json_encode($mData), - ZLIB_ENCODING_RAW, - 9 - ), - static::Passphrase($key) - ) - ); - } - public static function Decrypt(array $data, string $key = null) /* : mixed */ { if (3 === \count($data) && isset($data[0], $data[1], $data[2])) { - $fn = "{$data[0]}Decrypt"; - if (\method_exists(__CLASS__, $fn)) { - return \SnappyMail\Crypt::{$fn}($data[2], $data[1], $key); + try { + $fn = "{$data[0]}Decrypt"; + if (\method_exists(__CLASS__, $fn)) { + $result = static::{$fn}($data[2], $data[1], $key); + return \json_decode($result, true); + } + } catch (\Throwable $e) { + \trigger_error(__CLASS__ . "::{$fn}(): " . $e->getMessage()); } + } else { + \trigger_error(__CLASS__ . '::Decrypt() invalid $data'); } } public static function DecryptFromJSON(string $data, string $key = null) /* : mixed */ { - $aData = \json_decode($data, true); - return \is_array($aData) ? static::Decrypt(\array_map('base64_decode', $aData), $key) : null; + $data = \json_decode($data, true); + if (!\is_array($data)) { + \trigger_error(__CLASS__ . '::DecryptFromJSON() invalid $data'); + return null; + } + return static::Decrypt(\array_map('base64_decode', $data), $key); } - public static function Encrypt($data, string $key = null) : array + public static function DecryptUrlSafe(string $data, string $key = null) /* : mixed */ { - if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { - $nonce = \random_bytes(24); - return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce)]; + $data = \explode('.', $data); + if (!\is_array($data)) { + \trigger_error(__CLASS__ . '::DecryptUrlSafe() invalid $data'); + return null; } + return static::Decrypt(\array_map('MailSo\\Base\\Utils::UrlSafeBase64Decode', $data), $key); + } - if (static::$cipher && \is_callable('openssl_encrypt')) { + public static function Encrypt($data, string $key = null) : array + { + $data = \json_encode($data); + if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { + $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); + $result = ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)]; + } else if (static::$cipher && \is_callable('openssl_encrypt')) { $iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher)); - return ['openssl', $iv, static::OpenSSLEncrypt($data, $iv)]; + $result = ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)]; + } else { + $salt = \random_bytes(16); + $result = ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)]; } - - $salt = \random_bytes(16); - return ['xxtea', $salt, static::XxteaEncrypt($data, $salt)]; + if (static::{"{$result[0]}Decrypt"}($result[2], $result[1], $key) !== $data) { + throw new \Exception('Encrypt/Decrypt mismatch'); + } + return $result; } public static function EncryptToJSON($data, string $key = null) : string @@ -112,26 +109,31 @@ public static function EncryptToJSON($data, string $key = null) : string return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key))); } + public static function EncryptUrlSafe($data, string $key = null) : string + { + return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key))); + } + public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : mixed */ { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) { return null; } - return \json_decode(\zlib_decode(\sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( + return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt( $data, APP_SALT, $nonce, \str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) - ))); + ); } - public static function SodiumEncrypt($data, string $nonce, string $key = null) : ?string + public static function SodiumEncrypt(string $data, string $nonce, string $key = null) : ?string { if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) { return null; } return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt( - \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9), + $data, APP_SALT, $nonce, \str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key)) @@ -143,22 +145,22 @@ public static function OpenSSLDecrypt(string $data, string $iv, string $key = nu if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_decrypt')) { return null; } - return \json_decode(\zlib_decode(\openssl_decrypt( + return \openssl_decrypt( $data, static::$cipher, static::Passphrase($key), OPENSSL_RAW_DATA, $iv - )), true); + ); } - public static function OpenSSLEncrypt($data, string $iv, string $key = null) : ?string + public static function OpenSSLEncrypt(string $data, string $iv, string $key = null) : ?string { if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_encrypt')) { return null; } return \openssl_encrypt( - \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9), + $data, static::$cipher, static::Passphrase($key), OPENSSL_RAW_DATA, @@ -172,18 +174,16 @@ public static function XxteaDecrypt(string $data, string $salt, string $key = nu return null; } $key = $salt . static::Passphrase($key); - return \json_decode(\zlib_decode(\is_callable('xxtea_decrypt') + return \is_callable('xxtea_decrypt') ? \xxtea_decrypt($data, $key) - : \MailSo\Base\Xxtea::decrypt($data, $key) - ), true); + : \MailSo\Base\Xxtea::decrypt($data, $key); } - public static function XxteaEncrypt($data, string $salt, string $key = null) : ?string + public static function XxteaEncrypt(string $data, string $salt, string $key = null) : ?string { if (!$data || !$salt) { return null; } - $data = \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9); $key = $salt . static::Passphrase($key); return \is_callable('xxtea_encrypt') ? \xxtea_encrypt($data, $key) diff --git a/snappymail/v/0.0.0/app/templates/Views/User/PopupsAccount.html b/snappymail/v/0.0.0/app/templates/Views/User/PopupsAccount.html index 05c01759d4..e556d002cc 100644 --- a/snappymail/v/0.0.0/app/templates/Views/User/PopupsAccount.html +++ b/snappymail/v/0.0.0/app/templates/Views/User/PopupsAccount.html @@ -5,35 +5,33 @@

-